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/// * `permissions(list = "...", retrieve = "...", create = "...",
89/// update = "...", destroy = "...")` — codenames required per action.
90#[proc_macro_derive(ViewSet, attributes(viewset))]
91pub fn derive_viewset(input: TokenStream) -> TokenStream {
92 let input = parse_macro_input!(input as DeriveInput);
93 expand_viewset(&input)
94 .unwrap_or_else(syn::Error::into_compile_error)
95 .into()
96}
97
98/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
99/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
100/// that walks every named field and:
101///
102/// * Parses the string value into the field's Rust type (`String`,
103/// `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
104/// nullable case).
105/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
106/// `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
107/// validators in declaration order, returning `FormError::Parse`
108/// on the first failure.
109///
110/// Example:
111///
112/// ```ignore
113/// #[derive(Form)]
114/// pub struct CreateItemForm {
115/// #[form(min_length = 1, max_length = 64)]
116/// pub name: String,
117/// #[form(min = 0, max = 150)]
118/// pub age: i32,
119/// pub active: bool,
120/// pub email: Option<String>,
121/// }
122///
123/// let parsed = CreateItemForm::parse(&form_map)?;
124/// ```
125#[proc_macro_derive(Form, attributes(form))]
126pub fn derive_form(input: TokenStream) -> TokenStream {
127 let input = parse_macro_input!(input as DeriveInput);
128 expand_form(&input)
129 .unwrap_or_else(syn::Error::into_compile_error)
130 .into()
131}
132
133/// Derive `rustango::serializer::ModelSerializer` for a struct.
134/// (intra-doc link disabled — the macro crate doesn't depend on
135/// `rustango` itself, so rustdoc can't resolve the path.)
136///
137/// # Container attribute (required)
138/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
139///
140/// # Field attributes
141/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
142/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
143/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
144/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
145/// - `#[serializer(method = "fn_name")]` — DRF `SerializerMethodField`: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
146/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
147/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
148/// - `#[serializer(slug = "name")]` — DRF `SlugRelatedField`: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
149/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
150///
151/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
152#[proc_macro_derive(Serializer, attributes(serializer))]
153pub fn derive_serializer(input: TokenStream) -> TokenStream {
154 let input = parse_macro_input!(input as DeriveInput);
155 expand_serializer(&input)
156 .unwrap_or_else(syn::Error::into_compile_error)
157 .into()
158}
159
160/// Bake every `*.json` migration file in a directory into the binary
161/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
162/// of `(name, json_content)` pairs, lex-sorted by file stem.
163///
164/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
165/// behaviour as `migrate(pool, dir)` but with no filesystem access.
166/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
167/// (i.e. the crate that invokes the macro). Default is
168/// `"./migrations"` if no argument is supplied.
169///
170/// ```ignore
171/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
172/// // or:
173/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
174///
175/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
176/// ```
177///
178/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
179/// file's `name` field must equal its file stem, every `prev`
180/// reference must point to another migration in the same directory,
181/// and the JSON must parse. A broken chain — orphan `prev`, missing
182/// predecessor, malformed file — fails at macro-expansion time with
183/// a clear `compile_error!`. *No other Django-shape Rust framework
184/// validates migration chains at compile time*: Cot's migrations are
185/// imperative Rust code (no static chain), Loco's are SeaORM
186/// up/down (same), Rwf's are raw SQL (no chain at all).
187///
188/// Each migration is included via `include_str!` so cargo's rebuild
189/// detection picks up file *content* changes. **Caveat:** cargo
190/// doesn't watch directory listings, so adding or removing a
191/// migration file inside the dir won't auto-trigger a rebuild — run
192/// `cargo clean` (or just bump any other source file) when you add
193/// new migrations during embedded development.
194#[proc_macro]
195pub fn embed_migrations(input: TokenStream) -> TokenStream {
196 expand_embed_migrations(input.into())
197 .unwrap_or_else(syn::Error::into_compile_error)
198 .into()
199}
200
201/// `Q!()` — Django-shape filter syntax compile-time-resolved against
202/// typed columns. Issue #269 / T1.7.
203///
204/// Each invocation lowers to the equivalent typed-column method call:
205///
206/// ```ignore
207/// // These expand identically:
208/// Q!(User.email__icontains = "alice")
209/// User::email.ilike("%alice%")
210/// ```
211///
212/// Field-name typos fail the build (the macro emits `User::no_such_field`
213/// which doesn't exist) — the headline ergonomic win of this slice over
214/// Django's stringly-typed `__lookup` filters.
215///
216/// # Supported lookup suffixes
217///
218/// * bare `=` / `__exact` → `.eq(value)`
219/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
220/// * `__ne` → `.ne(value)`
221/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
222/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
223/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
224/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
225/// * `__in` → `.is_in(iterable)`
226/// * `__not_in` → `.not_in(iterable)`
227/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
228/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
229/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
230///
231/// Unknown suffixes fail the build with a `compile_error!` pointing at
232/// the lookup token.
233///
234/// # Combine
235///
236/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
237/// `.and()` / `.or()` / `.not()` methods:
238///
239/// ```ignore
240/// User::objects()
241/// .where_(
242/// Q!(User.active = true)
243/// .and(Q!(User.email__icontains = "alice"))
244/// )
245/// .fetch_pool(&pool).await?;
246/// ```
247///
248/// All emitted code routes through existing per-dialect writers — no new
249/// SQL emission machinery. Tri-dialect support is inherent.
250#[allow(non_snake_case)]
251#[proc_macro]
252pub fn Q(input: TokenStream) -> TokenStream {
253 expand_q(input.into())
254 .unwrap_or_else(syn::Error::into_compile_error)
255 .into()
256}
257
258/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
259/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
260/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
261/// functions are zero-boilerplate:
262///
263/// ```ignore
264/// #[rustango::main]
265/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
266/// rustango::server::Builder::from_env().await?
267/// .migrate("migrations").await?
268/// .api(my_app::urls::api())
269/// .seed_with(my_app::seed::run).await?
270/// .serve("0.0.0.0:8080").await
271/// }
272/// ```
273///
274/// Optional `flavor = "current_thread"` passes through to
275/// `#[tokio::main]`; default is the multi-threaded runtime.
276///
277/// Pulls `tracing-subscriber` into the rustango crate behind the
278/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
279/// out get plain `#[tokio::main]` ergonomics without the dependency.
280#[proc_macro_attribute]
281pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
282 expand_main(args.into(), item.into())
283 .unwrap_or_else(syn::Error::into_compile_error)
284 .into()
285}
286
287fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
288 let mut input: syn::ItemFn = syn::parse2(item)?;
289 if input.sig.asyncness.is_none() {
290 return Err(syn::Error::new(
291 input.sig.ident.span(),
292 "`#[rustango::main]` must wrap an `async fn`",
293 ));
294 }
295
296 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
297 // to `#[tokio::main]`. Tokio's proc-macro internally emits
298 // `::tokio::*` paths that resolve against the user crate's deps,
299 // so calling it through the rustango re-export still requires the
300 // user to add tokio to their own Cargo.toml. Building the
301 // runtime ourselves keeps the dep transitive through the
302 // `runtime` feature on rustango.
303 //
304 // Parse optional `flavor = "current_thread"` / `flavor =
305 // "multi_thread"` from the attribute args. Unknown args are
306 // tolerated (forward-compat with tokio's own arg surface).
307 let root = rustango_root();
308 let flavor = parse_flavor(&args);
309 let builder_call = match flavor {
310 Flavor::CurrentThread => quote! {
311 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
312 },
313 Flavor::MultiThread => quote! {
314 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
315 },
316 };
317
318 // Detach the user body and rewrite `main` as a sync fn that
319 // builds the runtime and blocks on the async body.
320 let user_body = input.block.clone();
321 input.sig.asyncness = None;
322 input.block = syn::parse2(quote! {{
323 {
324 use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
325 // `try_init` so duplicate installers (e.g. tests already
326 // holding a subscriber) don't panic.
327 let _ = tracing_subscriber::fmt()
328 .with_env_filter(
329 EnvFilter::try_from_default_env()
330 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
331 )
332 .try_init();
333 }
334 let __rt = #builder_call
335 .enable_all()
336 .build()
337 .expect("failed to build tokio runtime");
338 __rt.block_on(async move #user_body)
339 }})?;
340
341 Ok(quote! {
342 #input
343 })
344}
345
346enum Flavor {
347 MultiThread,
348 CurrentThread,
349}
350
351fn parse_flavor(args: &TokenStream2) -> Flavor {
352 // Cheap parser: look for the literal token sequence
353 // `flavor = "current_thread"`. Everything else (including
354 // bare `multi_thread` or no args) defaults to multi-thread.
355 let s = args.to_string();
356 if s.contains("current_thread") {
357 Flavor::CurrentThread
358 } else {
359 Flavor::MultiThread
360 }
361}
362
363/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
364struct QInput {
365 base_path: syn::Path,
366 field: syn::Ident,
367 value: syn::Expr,
368}
369
370impl syn::parse::Parse for QInput {
371 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
372 let base_path: syn::Path = input.parse()?;
373 input.parse::<syn::Token![.]>()?;
374 let field: syn::Ident = input.parse()?;
375 input.parse::<syn::Token![=]>()?;
376 let value: syn::Expr = input.parse()?;
377 Ok(QInput {
378 base_path,
379 field,
380 value,
381 })
382 }
383}
384
385fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
386 let q: QInput = syn::parse2(input)?;
387 let root = rustango_root();
388 let field_str = q.field.to_string();
389 let field_span = q.field.span();
390 let (base, suffix) = match field_str.find("__") {
391 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
392 None => (field_str.as_str(), ""),
393 };
394 if base.is_empty() {
395 return Err(syn::Error::new(
396 field_span,
397 "Q!(): field name is empty before `__` suffix",
398 ));
399 }
400 let base_ident = syn::Ident::new(base, field_span);
401 let value = &q.value;
402 let path = &q.base_path;
403
404 // Most suffixes map directly to a Column method with the value
405 // forwarded unchanged. Some need value-shape massaging (wildcards
406 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
407 // ISNULL). Unknown suffixes fail the build.
408 let expanded = match suffix {
409 "" | "exact" => quote! {
410 #root::core::Column::eq(#path::#base_ident, #value)
411 },
412 "ne" => quote! {
413 #root::core::Column::ne(#path::#base_ident, #value)
414 },
415 "gt" => quote! {
416 #root::core::Column::gt(#path::#base_ident, #value)
417 },
418 "gte" => quote! {
419 #root::core::Column::gte(#path::#base_ident, #value)
420 },
421 "lt" => quote! {
422 #root::core::Column::lt(#path::#base_ident, #value)
423 },
424 "lte" => quote! {
425 #root::core::Column::lte(#path::#base_ident, #value)
426 },
427 "iexact" => quote! {
428 // Django emulates `__iexact` as case-insensitive equality.
429 // The non-wildcard `ILIKE value` is semantically identical
430 // for plain strings; LIKE-metachars `%` `_` in the rhs would
431 // accidentally match more — document the caveat.
432 #root::core::Column::ilike(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
433 },
434 "contains" => quote! {
435 #root::core::Column::like(
436 #path::#base_ident,
437 ::std::format!("%{}%", #value),
438 )
439 },
440 "icontains" => quote! {
441 #root::core::Column::ilike(
442 #path::#base_ident,
443 ::std::format!("%{}%", #value),
444 )
445 },
446 "startswith" => quote! {
447 #root::core::Column::like(
448 #path::#base_ident,
449 ::std::format!("{}%", #value),
450 )
451 },
452 "istartswith" => quote! {
453 #root::core::Column::ilike(
454 #path::#base_ident,
455 ::std::format!("{}%", #value),
456 )
457 },
458 "endswith" => quote! {
459 #root::core::Column::like(
460 #path::#base_ident,
461 ::std::format!("%{}", #value),
462 )
463 },
464 "iendswith" => quote! {
465 #root::core::Column::ilike(
466 #path::#base_ident,
467 ::std::format!("%{}", #value),
468 )
469 },
470 "in" => quote! {
471 #root::core::Column::is_in(#path::#base_ident, #value)
472 },
473 "not_in" => quote! {
474 #root::core::Column::not_in(#path::#base_ident, #value)
475 },
476 "isnull" => {
477 // Must be a bool literal at macro time so we can route to
478 // is_null vs is_not_null without a runtime branch.
479 let b = match value {
480 syn::Expr::Lit(syn::ExprLit {
481 lit: syn::Lit::Bool(b),
482 ..
483 }) => b.value(),
484 _ => {
485 return Err(syn::Error::new_spanned(
486 value,
487 "Q!(): `__isnull` requires a `true` or `false` literal",
488 ));
489 }
490 };
491 if b {
492 quote! { #root::core::Column::is_null(#path::#base_ident) }
493 } else {
494 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
495 }
496 }
497 "between" => {
498 // Accept a tuple literal `(lo, hi)`.
499 let tuple = match value {
500 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
501 _ => {
502 return Err(syn::Error::new_spanned(
503 value,
504 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
505 ));
506 }
507 };
508 let lo = &tuple.elems[0];
509 let hi = &tuple.elems[1];
510 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
511 }
512 "regex" => quote! {
513 #root::core::Column::regex(#path::#base_ident, #value)
514 },
515 "iregex" => quote! {
516 #root::core::Column::iregex(#path::#base_ident, #value)
517 },
518 _ => {
519 return Err(syn::Error::new(
520 field_span,
521 format!(
522 "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",
523 suffix
524 ),
525 ));
526 }
527 };
528 Ok(expanded)
529}
530
531fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
532 // Default to "./migrations" if invoked without args.
533 let path_str = if input.is_empty() {
534 "./migrations".to_string()
535 } else {
536 let lit: LitStr = syn::parse2(input)?;
537 lit.value()
538 };
539
540 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
541 syn::Error::new(
542 proc_macro2::Span::call_site(),
543 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
544 )
545 })?;
546 let abs = std::path::Path::new(&manifest).join(&path_str);
547
548 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
549 if abs.is_dir() {
550 let read = std::fs::read_dir(&abs).map_err(|e| {
551 syn::Error::new(
552 proc_macro2::Span::call_site(),
553 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
554 )
555 })?;
556 for entry in read.flatten() {
557 let path = entry.path();
558 if !path.is_file() {
559 continue;
560 }
561 if path.extension().and_then(|s| s.to_str()) != Some("json") {
562 continue;
563 }
564 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
565 continue;
566 };
567 entries.push((stem.to_owned(), path));
568 }
569 }
570 entries.sort_by(|a, b| a.0.cmp(&b.0));
571
572 // Compile-time chain validation: read each migration's JSON,
573 // pull `name` and `prev` (file-stem-keyed for the chain check),
574 // and verify every `prev` points to another migration in the
575 // slice. Mismatches between the file stem and the embedded
576 // `name` field — and broken `prev` chains — fail at MACRO
577 // EXPANSION time so a misshapen migration set never compiles.
578 //
579 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
580 // migrations + a Rust proc-macro that reads them is the unique
581 // combo nothing else in the Django-shape Rust camp can match
582 // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
583 // Rwf's are raw SQL — none have a static chain to validate).
584 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
585 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
586 for (stem, path) in &entries {
587 let raw = std::fs::read_to_string(path).map_err(|e| {
588 syn::Error::new(
589 proc_macro2::Span::call_site(),
590 format!(
591 "embed_migrations!: cannot read {} for chain validation: {e}",
592 path.display()
593 ),
594 )
595 })?;
596 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
597 syn::Error::new(
598 proc_macro2::Span::call_site(),
599 format!(
600 "embed_migrations!: {} is not valid JSON: {e}",
601 path.display()
602 ),
603 )
604 })?;
605 let name = json
606 .get("name")
607 .and_then(|v| v.as_str())
608 .ok_or_else(|| {
609 syn::Error::new(
610 proc_macro2::Span::call_site(),
611 format!(
612 "embed_migrations!: {} is missing the `name` field",
613 path.display()
614 ),
615 )
616 })?
617 .to_owned();
618 if name != *stem {
619 return Err(syn::Error::new(
620 proc_macro2::Span::call_site(),
621 format!(
622 "embed_migrations!: file stem `{stem}` does not match the migration's \
623 `name` field `{name}` — rename the file or fix the JSON",
624 ),
625 ));
626 }
627 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
628 chain_names.push(name.clone());
629 prev_refs.push((name, prev));
630 }
631
632 let name_set: std::collections::HashSet<&str> =
633 chain_names.iter().map(String::as_str).collect();
634 for (name, prev) in &prev_refs {
635 if let Some(p) = prev {
636 if !name_set.contains(p.as_str()) {
637 return Err(syn::Error::new(
638 proc_macro2::Span::call_site(),
639 format!(
640 "embed_migrations!: broken migration chain — `{name}` declares \
641 prev=`{p}` but no migration with that name exists in {}",
642 abs.display()
643 ),
644 ));
645 }
646 }
647 }
648
649 let pairs: Vec<TokenStream2> = entries
650 .iter()
651 .map(|(name, path)| {
652 let path_lit = path.display().to_string();
653 quote! { (#name, ::core::include_str!(#path_lit)) }
654 })
655 .collect();
656
657 Ok(quote! {
658 {
659 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
660 __RUSTANGO_EMBEDDED
661 }
662 })
663}
664
665fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
666 let root = rustango_root();
667 let struct_name = &input.ident;
668
669 let Data::Struct(data) = &input.data else {
670 return Err(syn::Error::new_spanned(
671 struct_name,
672 "Model can only be derived on structs",
673 ));
674 };
675 let Fields::Named(named) = &data.fields else {
676 return Err(syn::Error::new_spanned(
677 struct_name,
678 "Model requires a struct with named fields",
679 ));
680 };
681
682 let container = parse_container_attrs(input)?;
683 let table = container
684 .table
685 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
686 let model_name = struct_name.to_string();
687
688 let collected = collect_fields(named, &table)?;
689
690 // Validate that #[rustango(display = "…")] names a real field.
691 if let Some((ref display, span)) = container.display {
692 if !collected.field_names.iter().any(|n| n == display) {
693 return Err(syn::Error::new(
694 span,
695 format!("`display = \"{display}\"` does not match any field on this struct"),
696 ));
697 }
698 }
699 let display = container.display.map(|(name, _)| name);
700 let app_label = container.app.clone();
701
702 // Validate admin field-name lists against declared field names.
703 // Note: `list_display` is intentionally NOT validated here. As of
704 // v0.32 it may also reference inventory-registered computed
705 // fields (via `register_admin_computed!`) whose existence the
706 // macro can't see at compile time — they're submitted from any
707 // crate that depends on rustango. The runtime list-view resolves
708 // unknown names against the inventory + silently drops the
709 // truly-bogus ones, which is the cheaper trade-off versus
710 // forcing a per-Model attr to opt out.
711 if let Some(admin) = &container.admin {
712 for (label, list) in [
713 ("search_fields", &admin.search_fields),
714 ("readonly_fields", &admin.readonly_fields),
715 ("list_filter", &admin.list_filter),
716 ] {
717 if let Some((names, span)) = list {
718 for name in names {
719 if !collected.field_names.iter().any(|n| n == name) {
720 return Err(syn::Error::new(
721 *span,
722 format!(
723 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
724 ),
725 ));
726 }
727 }
728 }
729 }
730 if let Some((pairs, span)) = &admin.ordering {
731 for (name, _) in pairs {
732 if !collected.field_names.iter().any(|n| n == name) {
733 return Err(syn::Error::new(
734 *span,
735 format!(
736 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
737 ),
738 ));
739 }
740 }
741 }
742 if let Some((groups, span)) = &admin.fieldsets {
743 for (_, fields) in groups {
744 for name in fields {
745 if !collected.field_names.iter().any(|n| n == name) {
746 return Err(syn::Error::new(
747 *span,
748 format!(
749 "`fieldsets`: \"{name}\" is not a declared field on this struct"
750 ),
751 ));
752 }
753 }
754 }
755 }
756 }
757 if let Some(audit) = &container.audit {
758 if let Some((names, span)) = &audit.track {
759 for name in names {
760 if !collected.field_names.iter().any(|n| n == name) {
761 return Err(syn::Error::new(
762 *span,
763 format!(
764 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
765 ),
766 ));
767 }
768 }
769 }
770 }
771
772 // Issue #291 / T2.5 — validate each `default_order` column name
773 // against the model's collected fields. Typos fail at macro-expand
774 // time, not at the database.
775 for (col, _desc, span) in &container.default_order {
776 if !collected.field_names.iter().any(|n| n == col) {
777 return Err(syn::Error::new(
778 *span,
779 format!(
780 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
781 ),
782 ));
783 }
784 }
785
786 // Build the audit_track list for ModelSchema: None when no audit attr,
787 // Some(empty) when audit present without track, Some(names) when explicit.
788 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
789 audit
790 .track
791 .as_ref()
792 .map(|(names, _)| names.clone())
793 .unwrap_or_default()
794 });
795
796 // Merge field-level indexes into the container's index list.
797 let mut all_indexes: Vec<IndexAttr> = container.indexes;
798 for field in &named.named {
799 let ident = field.ident.as_ref().expect("named");
800 let col = to_snake_case(&ident.to_string()); // column name fallback
801 // Re-parse field attrs to check for index flag
802 if let Ok(fa) = parse_field_attrs(field) {
803 if fa.index {
804 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
805 let auto_name = if fa.index_unique {
806 format!("{table}_{col_name}_uq_idx")
807 } else {
808 format!("{table}_{col_name}_idx")
809 };
810 all_indexes.push(IndexAttr {
811 name: fa.index_name.or(Some(auto_name)),
812 columns: vec![col_name],
813 unique: fa.index_unique,
814 method: fa.index_method,
815 where_clause: None,
816 include: Vec::new(),
817 });
818 }
819 }
820 }
821
822 let model_impl = model_impl_tokens(
823 struct_name,
824 &model_name,
825 &table,
826 display.as_deref(),
827 app_label.as_deref(),
828 container.admin.as_ref(),
829 &container.default_order,
830 &collected.field_schemas,
831 collected.soft_delete_column.as_deref(),
832 container.permissions,
833 audit_track_names.as_deref(),
834 &container.m2m,
835 &all_indexes,
836 &container.checks,
837 &container.excludes,
838 &container.composite_fks,
839 &container.generic_fks,
840 container.scope.as_deref(),
841 container.is_view,
842 container.verbose_name.as_deref(),
843 container.verbose_name_plural.as_deref(),
844 container.managed,
845 container.base_manager_name.as_deref(),
846 container.order_with_respect_to.as_deref(),
847 container.proxy,
848 &container.required_db_features,
849 container.required_db_vendor.as_deref(),
850 container.default_related_name.as_deref(),
851 container.db_table_comment.as_deref(),
852 container
853 .get_latest_by
854 .as_ref()
855 .map(|(c, d)| (c.as_str(), *d)),
856 &container.extra_permissions,
857 &container.default_permissions,
858 &container.global_scopes,
859 &container.reverse_has_relations,
860 &container.generic_has_relations,
861 );
862 let module_ident = column_module_ident(struct_name);
863 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
864 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
865 let track_set: Option<std::collections::HashSet<&str>> = audit
866 .track
867 .as_ref()
868 .map(|(names, _)| names.iter().map(String::as_str).collect());
869 collected
870 .column_entries
871 .iter()
872 .filter(|c| {
873 track_set
874 .as_ref()
875 .map_or(true, |s| s.contains(c.name.as_str()))
876 })
877 .collect()
878 });
879 let inherent_impl = inherent_impl_tokens(
880 struct_name,
881 &collected,
882 collected.primary_key.as_ref(),
883 &column_consts,
884 audited_fields.as_deref(),
885 &all_indexes,
886 &container.manager_fns,
887 );
888 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
889 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
890 let reverse_helpers = reverse_helper_tokens(
891 struct_name,
892 &collected.fk_relations,
893 container.default_related_name.as_deref(),
894 );
895 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
896 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
897 // Issue #817 — `#[rustango(through(...))]` accessors.
898 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
899 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
900 let reverse_has_accessors =
901 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
902 let generic_fk_accessors = generic_fk_accessor_tokens(
903 struct_name,
904 &container.generic_fks,
905 &collected.column_entries,
906 );
907
908 // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
909 // emits an empty extension trait so users can add methods via
910 // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
911 // trait declaration. See `crates/rustango/src/manager.rs` for the
912 // pattern this replaces.
913 let manager_trait = container.manager_ext.as_ref().map(|name| {
914 let model_name_str = struct_name.to_string();
915 let doc = format!(
916 "Custom-Manager extension trait for [`{model_name_str}`]. \
917 Generated by `#[rustango(manager(ext = ...))]`. Add methods \
918 via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
919 );
920 quote! {
921 #[doc = #doc]
922 pub trait #name: ::core::marker::Sized {}
923 }
924 });
925
926 Ok(quote! {
927 #model_impl
928 #inherent_impl
929 #from_row_impl
930 #column_module
931 #reverse_helpers
932 #m2m_accessors
933 #generic_m2m_accessors
934 #through_accessors
935 #reverse_has_accessors
936 #generic_fk_accessors
937 #manager_trait
938
939 #root::core::inventory::submit! {
940 #root::core::ModelEntry {
941 schema: <#struct_name as #root::core::Model>::SCHEMA,
942 // `module_path!()` evaluates at the registration site,
943 // so a Model declared in `crate::blog::models` records
944 // `"<crate>::blog::models"` and `resolved_app_label()`
945 // can infer "blog" without an explicit attribute.
946 module_path: ::core::module_path!(),
947 }
948 }
949 })
950}
951
952/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
953/// matches `field_name` against the model's FK fields and, for a
954/// match, decodes the FK target via the parent's macro-generated
955/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
956/// `ForeignKey::Loaded` on `self`.
957///
958/// Always emitted (with empty arms for FK-less models, which
959/// return `Ok(false)` for any field name) so the `T: LoadRelated`
960/// trait bound on `fetch_on` is universally satisfied — users
961/// never have to think about implementing it.
962fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
963 let root = rustango_root();
964 let arms = fk_relations.iter().map(|rel| {
965 let parent_ty = &rel.parent_type;
966 let fk_col = rel.fk_column.as_str();
967 // FK field's Rust ident matches its SQL column name in v0.8
968 // (no `column = "..."` rename ships on FK fields).
969 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
970 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
971 let assign = if rel.nullable {
972 quote! {
973 self.#field_ident = ::core::option::Option::Some(
974 #root::sql::ForeignKey::loaded(_pk, _parent),
975 );
976 }
977 } else {
978 quote! {
979 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
980 }
981 };
982 quote! {
983 #fk_col => {
984 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
985 // Audit #451 — multi-hop `select_related("a__b__c")`:
986 // stitch the deeper relation onto this parent first,
987 // decoding it at the accumulated `__next_alias`. The
988 // parent type also impls `LoadRelated`, so this recurses
989 // the FK chain to arbitrary depth.
990 if let ::core::option::Option::Some(__r) = __rest {
991 let _ = #root::sql::LoadRelated::__rustango_load_related(
992 &mut _parent, row, __r, &__next_alias,
993 )?;
994 }
995 // Loud-in-debug, default-in-release: a divergence
996 // between the FK field's declared `K` (drives the
997 // expected `SqlValue::<Variant>`) and the parent's
998 // `__rustango_pk_value` output is a macro-internal
999 // invariant break — surfacing the panic in dev
1000 // catches it before users hit silent PK=0 corruption.
1001 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1002 #root::core::SqlValue::#variant_ident(v) => v,
1003 _other => {
1004 ::core::debug_assert!(
1005 false,
1006 "rustango macro bug: load_related on FK `{}` expected \
1007 SqlValue::{} from parent's __rustango_pk_value but got \
1008 {:?} — file a bug at https://github.com/ujeenet/rustango",
1009 #fk_col,
1010 ::core::stringify!(#variant_ident),
1011 _other,
1012 );
1013 #default_expr
1014 }
1015 };
1016 #assign
1017 ::core::result::Result::Ok(true)
1018 }
1019 }
1020 });
1021 quote! {
1022 #[cfg(feature = "postgres")]
1023 impl #root::sql::LoadRelated for #struct_name {
1024 #[allow(unused_variables)]
1025 fn __rustango_load_related(
1026 &mut self,
1027 row: &#root::sql::sqlx::postgres::PgRow,
1028 field_name: &str,
1029 alias: &str,
1030 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1031 // Audit #451 — split the multi-hop path: `base` is the FK
1032 // on THIS model, `__rest` (if any) is the remaining chain
1033 // to stitch onto the loaded parent. `__next_alias` is the
1034 // accumulated join alias the parent's columns live under
1035 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1036 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1037 match field_name.split_once("__") {
1038 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1039 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1040 };
1041 let __next_alias: ::std::string::String = match __rest {
1042 ::core::option::Option::Some(__r) => {
1043 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1044 ::std::format!("{}__{}", alias, __rb)
1045 }
1046 ::core::option::Option::None => ::std::string::String::new(),
1047 };
1048 match __base {
1049 #( #arms )*
1050 _ => ::core::result::Result::Ok(false),
1051 }
1052 }
1053 }
1054 }
1055}
1056
1057/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1058/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1059/// which expands to a `LoadRelatedMy` impl when rustango is built with
1060/// the `mysql` feature, and to nothing otherwise. The decoded parent
1061/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1062/// decoder, also batch8) so the dual emission is symmetric across
1063/// backends.
1064fn load_related_impl_my_tokens(
1065 struct_name: &syn::Ident,
1066 fk_relations: &[FkRelation],
1067) -> TokenStream2 {
1068 let root = rustango_root();
1069 let arms = fk_relations.iter().map(|rel| {
1070 let parent_ty = &rel.parent_type;
1071 let fk_col = rel.fk_column.as_str();
1072 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1073 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1074 let assign = if rel.nullable {
1075 quote! {
1076 __self.#field_ident = ::core::option::Option::Some(
1077 #root::sql::ForeignKey::loaded(_pk, _parent),
1078 );
1079 }
1080 } else {
1081 quote! {
1082 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1083 }
1084 };
1085 // `self` IS hygiene-tracked through macro_rules — emitted from
1086 // a different context than the `&mut self` parameter inside
1087 // the macro_rules-expanded fn. Pass it through as `__self`
1088 // and let the macro_rules rebind it to the receiver.
1089 quote! {
1090 #fk_col => {
1091 let mut _parent: #parent_ty =
1092 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1093 // Audit #451 — multi-hop: stitch the deeper relation onto
1094 // the parent at the accumulated alias (see PG twin).
1095 if let ::core::option::Option::Some(__r) = __rest {
1096 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1097 &mut _parent, row, __r, &__next_alias,
1098 )?;
1099 }
1100 // See note in `load_related_impl_tokens` (PG twin) —
1101 // the same loud-in-debug invariant guard.
1102 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1103 #root::core::SqlValue::#variant_ident(v) => v,
1104 _other => {
1105 ::core::debug_assert!(
1106 false,
1107 "rustango macro bug: load_related on FK `{}` expected \
1108 SqlValue::{} from parent's __rustango_pk_value but got \
1109 {:?} — file a bug at https://github.com/ujeenet/rustango",
1110 #fk_col,
1111 ::core::stringify!(#variant_ident),
1112 _other,
1113 );
1114 #default_expr
1115 }
1116 };
1117 #assign
1118 ::core::result::Result::Ok(true)
1119 }
1120 }
1121 });
1122 quote! {
1123 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1124 #( #arms )*
1125 });
1126 }
1127}
1128
1129/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1130/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1131/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1132fn load_related_impl_sqlite_tokens(
1133 struct_name: &syn::Ident,
1134 fk_relations: &[FkRelation],
1135) -> TokenStream2 {
1136 let root = rustango_root();
1137 let arms = fk_relations.iter().map(|rel| {
1138 let parent_ty = &rel.parent_type;
1139 let fk_col = rel.fk_column.as_str();
1140 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1141 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1142 let assign = if rel.nullable {
1143 quote! {
1144 __self.#field_ident = ::core::option::Option::Some(
1145 #root::sql::ForeignKey::loaded(_pk, _parent),
1146 );
1147 }
1148 } else {
1149 quote! {
1150 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1151 }
1152 };
1153 quote! {
1154 #fk_col => {
1155 let mut _parent: #parent_ty =
1156 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1157 // Audit #451 — multi-hop: stitch the deeper relation onto
1158 // the parent at the accumulated alias (see PG twin).
1159 if let ::core::option::Option::Some(__r) = __rest {
1160 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1161 &mut _parent, row, __r, &__next_alias,
1162 )?;
1163 }
1164 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1165 #root::core::SqlValue::#variant_ident(v) => v,
1166 _other => {
1167 ::core::debug_assert!(
1168 false,
1169 "rustango macro bug: load_related on FK `{}` expected \
1170 SqlValue::{} from parent's __rustango_pk_value but got \
1171 {:?} — file a bug at https://github.com/ujeenet/rustango",
1172 #fk_col,
1173 ::core::stringify!(#variant_ident),
1174 _other,
1175 );
1176 #default_expr
1177 }
1178 };
1179 #assign
1180 ::core::result::Result::Ok(true)
1181 }
1182 }
1183 });
1184 quote! {
1185 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1186 #( #arms )*
1187 });
1188 }
1189}
1190
1191/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1192/// matches `field_name` against the model's FK fields and returns
1193/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1194/// group children by parent PK.
1195///
1196/// Always emitted (with `_ => None` for FK-less models) so the
1197/// trait bound on `fetch_with_prefetch` is universally satisfied.
1198fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1199 let root = rustango_root();
1200 let arms = fk_relations.iter().map(|rel| {
1201 let fk_col = rel.fk_column.as_str();
1202 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1203 if rel.pk_kind == DetectedKind::I64 {
1204 // i64 FK — return the stored PK so prefetch_related can
1205 // group children by it. Nullable variant unwraps via
1206 // `as_ref().map(...)`: an unset (NULL) FK column yields
1207 // `None` and that child sits out of the grouping (correct
1208 // semantics — it has no parent to attach to).
1209 if rel.nullable {
1210 quote! {
1211 #fk_col => self.#field_ident
1212 .as_ref()
1213 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1214 }
1215 } else {
1216 quote! {
1217 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1218 }
1219 }
1220 } else {
1221 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1222 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1223 // i64-keyed grouping path — the trait signature is
1224 // `Option<i64>` and a non-i64 PK can't lower into it.
1225 // The FK still works for everything else (CRUD, lazy
1226 // load via `.get()`, select_related JOINs); only the
1227 // bulk prefetch grouper needs the integer key.
1228 quote! {
1229 #fk_col => ::core::option::Option::None,
1230 }
1231 }
1232 });
1233 // PK-type-agnostic version: every FK arm emits an
1234 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1235 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1236 // opt OUT of the legacy i64 method (it returns None) but opt IN
1237 // here.
1238 let value_arms = fk_relations.iter().map(|rel| {
1239 let fk_col = rel.fk_column.as_str();
1240 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1241 if rel.nullable {
1242 quote! {
1243 #fk_col => self.#field_ident
1244 .as_ref()
1245 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1246 #root::sql::ForeignKey::pk(fk)
1247 )),
1248 }
1249 } else {
1250 quote! {
1251 #fk_col => ::core::option::Option::Some(
1252 ::core::convert::Into::<#root::core::SqlValue>::into(
1253 self.#field_ident.pk()
1254 )
1255 ),
1256 }
1257 }
1258 });
1259 quote! {
1260 impl #root::sql::FkPkAccess for #struct_name {
1261 #[allow(unused_variables)]
1262 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1263 match field_name {
1264 #( #arms )*
1265 _ => ::core::option::Option::None,
1266 }
1267 }
1268 #[allow(unused_variables)]
1269 fn __rustango_fk_pk_value(
1270 &self,
1271 field_name: &str,
1272 ) -> ::core::option::Option<#root::core::SqlValue> {
1273 match field_name {
1274 #( #value_arms )*
1275 _ => ::core::option::Option::None,
1276 }
1277 }
1278 }
1279 }
1280}
1281
1282/// For every `ForeignKey<Parent>` field on `Child`, emit
1283/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1284/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1285/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1286/// — the canonical reverse-FK fetch. One round trip, no N+1.
1287///
1288/// **PG-only emission**: the accessor is bounded on
1289/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1290/// both of which are gated behind the `postgres` cargo feature. The
1291/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1292/// model derive itself compiles on tri-dialect / sqlite-only
1293/// downstream builds — the accessor just isn't materialised. A tri-
1294/// dialect `_set_pool` variant is a separate follow-up.
1295fn reverse_helper_tokens(
1296 child_ident: &syn::Ident,
1297 fk_relations: &[FkRelation],
1298 default_related_name: Option<&str>,
1299) -> TokenStream2 {
1300 let root = rustango_root();
1301 if fk_relations.is_empty() {
1302 return TokenStream2::new();
1303 }
1304 // Method-name resolution per FK (issue #816 + follow-up):
1305 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1306 // itself — wins over everything else. Django's
1307 // `ForeignKey(related_name="...")`.
1308 // 2. Container-level `default_related_name = "..."` on the
1309 // child — Django's `class Meta: default_related_name`.
1310 // Applies to every FK on this model that didn't override.
1311 // 3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1312 // convention. `Post` → `post_set`, `BlogComment` →
1313 // `blog_comment_set`. Avoids English-plural edge cases.
1314 //
1315 // The PG-on-executor variant keeps the resolved name; the
1316 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1317 // framework's convention for the `&Pool` flavor of every helper).
1318 let default_pg_suffix = default_related_name
1319 .map(str::to_owned)
1320 .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1321 let impls = fk_relations.iter().map(|rel| {
1322 let pg_suffix = rel
1323 .related_name
1324 .clone()
1325 .unwrap_or_else(|| default_pg_suffix.clone());
1326 let pool_suffix = format!("{}_pool", pg_suffix);
1327 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1328 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1329 let parent_ty = &rel.parent_type;
1330 let fk_col = rel.fk_column.as_str();
1331 let doc = format!(
1332 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1333 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1334 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1335 further `{child_ident}::objects()` filters via direct queryset use."
1336 );
1337 let pool_doc = format!(
1338 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1339 [`#root::sql::Pool`] and dispatches per backend so the \
1340 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1341 Use this from framework code that holds a `&Pool` (admin, \
1342 tenancy resolver, viewset handlers); reach for the executor- \
1343 bound variant when you already have a typed `sqlx::Executor`."
1344 );
1345 quote! {
1346 #[cfg(feature = "postgres")]
1347 impl #parent_ty {
1348 #[doc = #doc]
1349 ///
1350 /// # Errors
1351 /// Returns [`#root::sql::ExecError`] for SQL-writing
1352 /// or driver failures.
1353 pub async fn #pg_method_ident<'_c, _E>(
1354 &self,
1355 _executor: _E,
1356 ) -> ::core::result::Result<
1357 ::std::vec::Vec<#child_ident>,
1358 #root::sql::ExecError,
1359 >
1360 where
1361 _E: #root::sql::sqlx::Executor<
1362 '_c,
1363 Database = #root::sql::sqlx::Postgres,
1364 >,
1365 {
1366 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1367 #root::query::QuerySet::<#child_ident>::new()
1368 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1369 .fetch_on(_executor)
1370 .await
1371 }
1372 }
1373
1374 impl #parent_ty {
1375 #[doc = #pool_doc]
1376 ///
1377 /// # Errors
1378 /// Returns [`#root::sql::ExecError`] for SQL-writing
1379 /// or driver failures.
1380 pub async fn #pool_method_ident(
1381 &self,
1382 pool: &#root::sql::Pool,
1383 ) -> ::core::result::Result<
1384 ::std::vec::Vec<#child_ident>,
1385 #root::sql::ExecError,
1386 > {
1387 use #root::sql::FetcherPool as _;
1388 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1389 #root::query::QuerySet::<#child_ident>::new()
1390 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1391 .fetch_pool(pool)
1392 .await
1393 }
1394 }
1395 }
1396 });
1397 quote! { #( #impls )* }
1398}
1399
1400/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1401/// relation declared on the model.
1402/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1403/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1404///
1405/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1406/// `comment.content_object = post` ergonomics on top of the existing
1407/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1408///
1409/// `column_entries` is passed so we can resolve each `ct_column` /
1410/// `pk_column` SQL name back to its Rust field ident — the macro
1411/// only sees the column-side strings in the attribute, but the
1412/// emitted accessor needs to read the actual struct field.
1413fn generic_fk_accessor_tokens(
1414 struct_name: &syn::Ident,
1415 generic_fks: &[GenericFkAttr],
1416 column_entries: &[ColumnEntry],
1417) -> TokenStream2 {
1418 let root = rustango_root();
1419 if generic_fks.is_empty() {
1420 return TokenStream2::new();
1421 }
1422 let methods = generic_fks.iter().filter_map(|gfk| {
1423 // Resolve `ct_column` + `pk_column` to the struct's Rust
1424 // field idents. A typo (column name doesn't match any field)
1425 // emits no method for that registration — the user will see
1426 // the compiler reject the SCHEMA literal anyway, so there's
1427 // a clear error path without us double-reporting.
1428 let ct_ident = column_entries
1429 .iter()
1430 .find(|c| c.column == gfk.ct_column)
1431 .map(|c| c.ident.clone())?;
1432 let pk_ident = column_entries
1433 .iter()
1434 .find(|c| c.column == gfk.pk_column)
1435 .map(|c| c.ident.clone())?;
1436
1437 let accessor_ident =
1438 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1439 let setter_ident =
1440 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1441 let name_literal = gfk.name.as_str();
1442
1443 Some(quote! {
1444 #[doc = concat!(
1445 "Resolve the polymorphic `",
1446 #name_literal,
1447 "` relation. Reads `self.",
1448 stringify!(#ct_ident),
1449 "` + `self.",
1450 stringify!(#pk_ident),
1451 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1452 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1453 #name_literal,
1454 "\", ...))]`."
1455 )]
1456 pub async fn #accessor_ident(
1457 &self,
1458 pool: &#root::sql::Pool,
1459 ) -> ::core::result::Result<
1460 ::core::option::Option<#root::__serde_json::Value>,
1461 #root::sql::ExecError,
1462 > {
1463 let gfk = #root::contenttypes::GenericForeignKey::new(
1464 self.#ct_ident as i64,
1465 self.#pk_ident as i64,
1466 );
1467 gfk.get_object(pool).await
1468 }
1469
1470 #[doc = concat!(
1471 "Set the polymorphic `",
1472 #name_literal,
1473 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1474 stringify!(#ct_ident),
1475 "` and `self.",
1476 stringify!(#pk_ident),
1477 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1478 #name_literal,
1479 "\", ...))]`."
1480 )]
1481 pub async fn #setter_ident<T: #root::core::Model>(
1482 &mut self,
1483 pool: &#root::sql::Pool,
1484 target_pk: i64,
1485 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1486 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1487 pool,
1488 target_pk,
1489 ).await?;
1490 self.#ct_ident = gfk.content_type_id as _;
1491 self.#pk_ident = gfk.object_pk as _;
1492 ::core::result::Result::Ok(())
1493 }
1494 })
1495 });
1496 quote! {
1497 impl #struct_name {
1498 #( #methods )*
1499 }
1500 }
1501}
1502
1503fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1504 let root = rustango_root();
1505 if m2m_relations.is_empty() {
1506 return TokenStream2::new();
1507 }
1508 let methods = m2m_relations.iter().map(|rel| {
1509 let method_name = format!("{}_m2m", rel.name);
1510 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1511 let through = rel.through.as_str();
1512 let src_col = rel.src.as_str();
1513 let dst_col = rel.dst.as_str();
1514 quote! {
1515 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1516 #root::sql::M2MManager {
1517 src_pk: self.__rustango_pk_value(),
1518 through: #through,
1519 src_col: #src_col,
1520 dst_col: #dst_col,
1521 }
1522 }
1523 }
1524 });
1525 quote! {
1526 impl #struct_name {
1527 #( #methods )*
1528 }
1529 }
1530}
1531
1532/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1533/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1534fn generic_m2m_accessor_tokens(
1535 struct_name: &syn::Ident,
1536 relations: &[GenericM2MAttr],
1537) -> TokenStream2 {
1538 let root = rustango_root();
1539 if relations.is_empty() {
1540 return TokenStream2::new();
1541 }
1542 let methods = relations.iter().map(|rel| {
1543 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1544 let through = rel.through.as_str();
1545 let pk_col = rel.pk_column.as_str();
1546 let ct_col = rel.ct_column.as_str();
1547 let related_col = rel.related_column.as_str();
1548 quote! {
1549 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1550 #root::sql::GenericM2MManager {
1551 src_pk: self.__rustango_pk_value(),
1552 src_schema: <Self as #root::core::Model>::SCHEMA,
1553 through: #through,
1554 pk_col: #pk_col,
1555 ct_col: #ct_col,
1556 dst_col: #related_col,
1557 }
1558 }
1559 }
1560 });
1561 quote! {
1562 impl #struct_name {
1563 #( #methods )*
1564 }
1565 }
1566}
1567
1568/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1569/// associated functions for each `#[rustango(reverse_has(...))]`
1570/// attribute. Issue #830.
1571///
1572/// The two emitted functions return ready-to-use `WhereExpr` nodes
1573/// that downstream callers drop into
1574/// `QuerySet::<Self>::where_raw(...)`:
1575///
1576/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1577/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1578/// `whereHas` parity (without the closure-style sub-predicate
1579/// refinement — that's a follow-up; users can layer additional
1580/// predicates by constructing the SelectQuery themselves and
1581/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1582/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1583/// `whereDoesntHave` parity.
1584///
1585/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1586/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1587/// machinery threads the outer-table reference through automatically
1588/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1589fn reverse_has_accessor_tokens(
1590 struct_name: &syn::Ident,
1591 reverse_has_relations: &[ReverseHasAttr],
1592) -> TokenStream2 {
1593 let root = rustango_root();
1594 if reverse_has_relations.is_empty() {
1595 return TokenStream2::new();
1596 }
1597 let methods = reverse_has_relations.iter().map(|rel| {
1598 let exists_name = format!("{}_exists_expr", rel.name);
1599 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1600 let count_name = format!("{}_count", rel.name);
1601 let fetch_name = format!("{}_fetch", rel.name);
1602 let first_name = format!("{}_first", rel.name);
1603 let pluck_name = format!("{}_pluck", rel.name);
1604 let accessor_name = rel.name.as_str();
1605 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1606 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1607 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1608 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1609 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1610 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1611 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1612 let child = &rel.child;
1613 let child_fk_column = rel.child_fk_column.as_str();
1614 let self_pk_column = rel.self_pk_column.as_str();
1615 let exists_doc = format!(
1616 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1617 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1618 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1619 filter to {struct_name}s with at least one matching child.",
1620 );
1621 let not_exists_doc = format!(
1622 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1623 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1624 <outer>.{self_pk_column})`. Drop into \
1625 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1626 {struct_name}s with **no** matching child.",
1627 );
1628 let count_doc = format!(
1629 "Eloquent `$model->{name}->count()` analog — returns \
1630 the number of `{child}` rows whose `{child_fk_column}` \
1631 matches this `{struct_name}` instance's primary key. \
1632 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1633 {child_fk_column} = <self.pk>`.",
1634 name = rel.name,
1635 );
1636 let accessor_doc = format!(
1637 "Eloquent `$model->{name}` accessor — returns a \
1638 `QuerySet<{child}>` filtered to rows whose \
1639 `{child_fk_column}` matches this `{struct_name}` \
1640 instance's primary key. **Chainable**: compose `.filter()` \
1641 / `.order_by()` / `.limit()` etc. on top, then call \
1642 `.fetch_pool(&pool)` (the QuerySet trait method) when \
1643 done. For the simple \"fetch all\" hot path with no \
1644 further composition, prefer the bare-name \
1645 `{name}_fetch(&pool)` companion.",
1646 name = rel.name,
1647 );
1648 let fetch_doc = format!(
1649 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1650 over `{name}(&self).fetch_pool(&pool)`. Use this when \
1651 you don't need further `.filter()` / `.order_by()` \
1652 composition; falls back to the chainable accessor when \
1653 you do. Avoids the `_pool` suffix on the most common \
1654 call-site shape.",
1655 name = rel.name,
1656 );
1657 quote! {
1658 #[doc = #accessor_doc]
1659 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1660 #root::query::QuerySet::<#child>::new()
1661 .filter(#child_fk_column, self.__rustango_pk_value())
1662 }
1663
1664 #[doc = #fetch_doc]
1665 pub async fn #fetch_ident(
1666 &self,
1667 pool: &#root::sql::Pool,
1668 ) -> ::core::result::Result<
1669 ::std::vec::Vec<#child>,
1670 #root::sql::ExecError,
1671 > {
1672 use #root::sql::FetcherPool as _;
1673 self.#accessor_ident().fetch_pool(pool).await
1674 }
1675
1676 /// Eloquent `$model->relation->first()` / `hasOne`
1677 /// semantics — bare-name shortcut over
1678 /// `self.<name>().first(&pool)`. Returns `None` when no
1679 /// child rows match. Useful when the relation is
1680 /// nominally many-to-one in shape but at most one row is
1681 /// expected (latest comment, primary tag, etc.).
1682 pub async fn #first_ident(
1683 &self,
1684 pool: &#root::sql::Pool,
1685 ) -> ::core::result::Result<
1686 ::core::option::Option<#child>,
1687 #root::sql::ExecError,
1688 > {
1689 self.#accessor_ident().first(pool).await
1690 }
1691
1692 /// Eloquent `$model->relation->pluck($col)` — project a
1693 /// single column from the child rows into a `Vec<U>`.
1694 /// Skips the typed `Child` decode, which is cheaper when
1695 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1696 pub async fn #pluck_ident<U>(
1697 &self,
1698 col: &'static str,
1699 pool: &#root::sql::Pool,
1700 ) -> ::core::result::Result<
1701 ::std::vec::Vec<U>,
1702 #root::sql::ExecError,
1703 >
1704 where
1705 U: #root::sql::MaybePgScalar
1706 + #root::sql::MaybeMyScalar
1707 + #root::sql::MaybeSqliteScalar
1708 + ::core::marker::Send
1709 + ::core::marker::Unpin,
1710 {
1711 self.#accessor_ident()
1712 .values_list_flat(col)
1713 .fetch::<U>(pool)
1714 .await
1715 }
1716
1717 #[doc = #exists_doc]
1718 pub fn #exists_ident() -> #root::core::WhereExpr {
1719 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1720 let child_schema =
1721 <#child as #root::core::Model>::SCHEMA;
1722 let inner = SelectQuery {
1723 where_clause: WhereExpr::ExprCompare {
1724 lhs: Expr::Column(#child_fk_column),
1725 op: Op::Eq,
1726 rhs: Expr::OuterRef(#self_pk_column),
1727 },
1728 ..SelectQuery::new(child_schema)
1729 };
1730 WhereExpr::Exists(::std::boxed::Box::new(inner))
1731 }
1732
1733 #[doc = #not_exists_doc]
1734 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1735 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1736 let child_schema =
1737 <#child as #root::core::Model>::SCHEMA;
1738 let inner = SelectQuery {
1739 where_clause: WhereExpr::ExprCompare {
1740 lhs: Expr::Column(#child_fk_column),
1741 op: Op::Eq,
1742 rhs: Expr::OuterRef(#self_pk_column),
1743 },
1744 ..SelectQuery::new(child_schema)
1745 };
1746 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1747 }
1748
1749 #[doc = #count_doc]
1750 pub async fn #count_ident(
1751 &self,
1752 pool: &#root::sql::Pool,
1753 ) -> ::core::result::Result<
1754 i64,
1755 #root::sql::ExecError,
1756 > {
1757 use #root::sql::CounterPool as _;
1758 #root::query::QuerySet::<#child>::new()
1759 .filter(#child_fk_column, self.__rustango_pk_value())
1760 .count_pool(pool)
1761 .await
1762 }
1763 }
1764 });
1765 quote! {
1766 impl #struct_name {
1767 #( #methods )*
1768 }
1769 }
1770}
1771
1772/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1773/// `#[rustango(through(...))]` attribute. Issue #817.
1774///
1775/// Each accessor builds a correlated subquery via
1776/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1777/// intermediate table, filters on the FK column pointing at this
1778/// model, and projects the intermediate PK. The outer queryset
1779/// filters the far model by its FK-to-intermediate column being in
1780/// that set.
1781///
1782/// The returned `QuerySet<Far>` is **chainable** — the subquery
1783/// lives inside a `where_raw` clause so the user's later
1784/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1785/// disturb it.
1786///
1787/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1788/// SQLite — no LATERAL or backend-specific syntax involved.
1789fn through_accessor_tokens(
1790 struct_name: &syn::Ident,
1791 through_relations: &[ThroughAttr],
1792) -> TokenStream2 {
1793 let root = rustango_root();
1794 if through_relations.is_empty() {
1795 return TokenStream2::new();
1796 }
1797 let methods = through_relations.iter().map(|rel| {
1798 let method_name = format!("{}_through", rel.name);
1799 let count_name = format!("{}_through_count", rel.name);
1800 let fetch_name = format!("{}_through_fetch", rel.name);
1801 let first_name = format!("{}_through_first", rel.name);
1802 let pluck_name = format!("{}_through_pluck", rel.name);
1803 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1804 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1805 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1806 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1807 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1808 let far = &rel.far;
1809 let intermediate = &rel.intermediate;
1810 let far_fk_column = rel.far_fk_column.as_str();
1811 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1812 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1813 let doc = format!(
1814 "Eloquent `hasManyThrough` accessor — returns a \
1815 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1816 instance through the intermediate `{intermediate}` table. \
1817 Generated SQL shape: \
1818 `… WHERE {far_fk_column} IN (SELECT \
1819 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1820 {intermediate_fk_column} = self.pk)`. Chainable like any \
1821 other QuerySet — compose `.filter()` / `.order_by()` / \
1822 `.limit()` etc. on top.",
1823 );
1824 let count_doc = format!(
1825 "Eloquent `$model->{name}->count()` analog for the \
1826 through-relation — returns the number of `{far}` rows \
1827 reachable through `{intermediate}`. Equivalent to \
1828 `self.{name}_through().count_pool(pool)` but spelled \
1829 as a bare instance method for parity with the \
1830 `reverse_has` `<name>_count` shape.",
1831 name = rel.name,
1832 );
1833 quote! {
1834 #[doc = #doc]
1835 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1836 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1837 let intermediate_schema =
1838 <#intermediate as #root::core::Model>::SCHEMA;
1839 let sub = SelectQuery {
1840 where_clause: WhereExpr::Predicate(Filter {
1841 column: #intermediate_fk_column,
1842 op: Op::Eq,
1843 value: self.__rustango_pk_value(),
1844 }),
1845 projection: ::core::option::Option::Some(
1846 ::std::vec![#intermediate_pk_column],
1847 ),
1848 ..SelectQuery::new(intermediate_schema)
1849 };
1850 #root::query::QuerySet::<#far>::new().where_raw(
1851 WhereExpr::InSubquery {
1852 column: #far_fk_column,
1853 negated: false,
1854 subquery: ::std::boxed::Box::new(sub),
1855 },
1856 )
1857 }
1858
1859 #[doc = #count_doc]
1860 pub async fn #count_ident(
1861 &self,
1862 pool: &#root::sql::Pool,
1863 ) -> ::core::result::Result<
1864 i64,
1865 #root::sql::ExecError,
1866 > {
1867 use #root::sql::CounterPool as _;
1868 self.#method_ident().count_pool(pool).await
1869 }
1870
1871 /// Eloquent `$model->relation->get()` for the
1872 /// through-relation — bare-name hot-path over
1873 /// `self.<name>_through().fetch_pool(&pool)`. Use this
1874 /// when you don't need further `.filter()` /
1875 /// `.order_by()` composition; falls back to the
1876 /// chainable accessor when you do. Avoids the `_pool`
1877 /// suffix on the most common call-site shape.
1878 pub async fn #fetch_ident(
1879 &self,
1880 pool: &#root::sql::Pool,
1881 ) -> ::core::result::Result<
1882 ::std::vec::Vec<#far>,
1883 #root::sql::ExecError,
1884 > {
1885 use #root::sql::FetcherPool as _;
1886 self.#method_ident().fetch_pool(pool).await
1887 }
1888
1889 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1890 /// over `self.<name>_through().first(&pool)`. Returns
1891 /// `None` when no far rows are reachable through the
1892 /// intermediate. Useful when at most one row is
1893 /// expected (latest comment by country, primary tag,
1894 /// etc.).
1895 pub async fn #first_ident(
1896 &self,
1897 pool: &#root::sql::Pool,
1898 ) -> ::core::result::Result<
1899 ::core::option::Option<#far>,
1900 #root::sql::ExecError,
1901 > {
1902 self.#method_ident().first(pool).await
1903 }
1904
1905 /// Pluck a single column from the far rows into `Vec<U>`
1906 /// — cheaper than the typed `<Far>` decode when only one
1907 /// scalar column is needed.
1908 pub async fn #pluck_ident<U>(
1909 &self,
1910 col: &'static str,
1911 pool: &#root::sql::Pool,
1912 ) -> ::core::result::Result<
1913 ::std::vec::Vec<U>,
1914 #root::sql::ExecError,
1915 >
1916 where
1917 U: #root::sql::MaybePgScalar
1918 + #root::sql::MaybeMyScalar
1919 + #root::sql::MaybeSqliteScalar
1920 + ::core::marker::Send
1921 + ::core::marker::Unpin,
1922 {
1923 self.#method_ident()
1924 .values_list_flat(col)
1925 .fetch::<U>(pool)
1926 .await
1927 }
1928 }
1929 });
1930 quote! {
1931 impl #struct_name {
1932 #( #methods )*
1933 }
1934 }
1935}
1936
1937struct ColumnEntry {
1938 /// The struct field ident, used both for the inherent const name on
1939 /// the model and for the inner column type's name.
1940 ident: syn::Ident,
1941 /// The struct's field type, used as `Column::Value`.
1942 value_ty: Type,
1943 /// Rust-side field name (e.g. `"id"`).
1944 name: String,
1945 /// SQL-side column name (e.g. `"user_id"`).
1946 column: String,
1947 /// `#root::core::FieldType::I64` etc.
1948 field_type_tokens: TokenStream2,
1949}
1950
1951struct CollectedFields {
1952 field_schemas: Vec<TokenStream2>,
1953 from_row_inits: Vec<TokenStream2>,
1954 /// Aliased counterparts of `from_row_inits` — read columns via
1955 /// `format!("{prefix}__{col}")` aliases so a Model can be
1956 /// decoded from a JOINed row's projected target columns.
1957 from_aliased_row_inits: Vec<TokenStream2>,
1958 /// Static column-name list — used by the simple insert path
1959 /// (no `Auto<T>` fields). Aligned with `insert_values`.
1960 insert_columns: Vec<TokenStream2>,
1961 /// Static `Into<SqlValue>` expressions, one per field. Aligned
1962 /// with `insert_columns`. Used by the simple insert path only.
1963 insert_values: Vec<TokenStream2>,
1964 /// Per-field push expressions for the dynamic (Auto-aware)
1965 /// insert path. Each statement either unconditionally pushes
1966 /// `(column, value)` or, for an `Auto<T>` field, conditionally
1967 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1968 insert_pushes: Vec<TokenStream2>,
1969 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1970 /// when `has_auto == false`.
1971 returning_cols: Vec<TokenStream2>,
1972 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1973 /// field. Run after `insert_returning` to populate the model.
1974 auto_assigns: Vec<TokenStream2>,
1975 /// `(ident, column_literal)` pairs for every Auto field. Used by
1976 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1977 /// instead of `self`.
1978 auto_field_idents: Vec<(syn::Ident, String)>,
1979 /// Inner `T` of the first `Auto<T>` field, for the MySQL
1980 /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1981 first_auto_value_ty: Option<Type>,
1982 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1983 /// by the all-Auto-Unset bulk path (Auto cols dropped from
1984 /// `columns`).
1985 bulk_pushes_no_auto: Vec<TokenStream2>,
1986 /// Bulk-insert per-row pushes for **all fields including Auto**.
1987 /// Used by the all-Auto-Set bulk path (Auto col included with the
1988 /// caller-supplied value).
1989 bulk_pushes_all: Vec<TokenStream2>,
1990 /// Column-name literals for non-Auto fields only (paired with
1991 /// `bulk_pushes_no_auto`).
1992 bulk_columns_no_auto: Vec<TokenStream2>,
1993 /// Column-name literals for every field including Auto (paired
1994 /// with `bulk_pushes_all`).
1995 bulk_columns_all: Vec<TokenStream2>,
1996 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
1997 /// + the loop that asserts every row matches. One pair per Auto
1998 /// field. Empty when `has_auto == false`.
1999 bulk_auto_uniformity: Vec<TokenStream2>,
2000 /// Identifier of the first Auto field, used as the witness for
2001 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2002 first_auto_ident: Option<syn::Ident>,
2003 /// `true` if any field on the struct is `Auto<T>`.
2004 has_auto: bool,
2005 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2006 /// Gates `save()` codegen — only Auto PKs let us infer
2007 /// insert-vs-update from the in-memory value.
2008 pk_is_auto: bool,
2009 /// `Assignment` constructors for every non-PK column. Drives the
2010 /// UPDATE branch of `save()`.
2011 update_assignments: Vec<TokenStream2>,
2012 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2013 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2014 upsert_update_columns: Vec<TokenStream2>,
2015 primary_key: Option<(syn::Ident, String)>,
2016 column_entries: Vec<ColumnEntry>,
2017 /// Rust-side field names, in declaration order. Used to validate
2018 /// container attributes like `display = "…"`.
2019 field_names: Vec<String>,
2020 /// FK fields on this child model. Drives the reverse-relation
2021 /// helper emit — for each FK, the macro adds an inherent
2022 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2023 /// method on the parent type.
2024 fk_relations: Vec<FkRelation>,
2025 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2026 /// the model has one. Drives emission of the `soft_delete_on` /
2027 /// `restore_on` inherent methods. At most one such column per
2028 /// model is allowed; collect_fields rejects duplicates.
2029 soft_delete_column: Option<String>,
2030 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2031 /// companion to `soft_delete_column` for emitting predicates
2032 /// that need to read the field off `&self` (e.g. `trashed()`).
2033 soft_delete_field_ident: Option<syn::Ident>,
2034}
2035
2036#[derive(Clone)]
2037struct FkRelation {
2038 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2039 /// helper is emitted as `impl <ParentType> { … }`.
2040 parent_type: Type,
2041 /// SQL column name on the child table for this FK (e.g. `"author"`).
2042 /// Used in the generated `WHERE <fk_column> = $1` clause.
2043 fk_column: String,
2044 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2045 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2046 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2047 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2048 pk_kind: DetectedKind,
2049 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2050 /// FK column). Drives the `Some(...)` wrapping in load_related
2051 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2052 /// the codegen matches the field's declared shape.
2053 nullable: bool,
2054 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2055 /// override. When set, the reverse helper picks this name instead
2056 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2057 /// #816 (issue's "Related" note re: per-FK override).
2058 related_name: Option<String>,
2059}
2060
2061fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2062 let root = rustango_root();
2063 let cap = named.named.len();
2064 let mut out = CollectedFields {
2065 field_schemas: Vec::with_capacity(cap),
2066 from_row_inits: Vec::with_capacity(cap),
2067 from_aliased_row_inits: Vec::with_capacity(cap),
2068 insert_columns: Vec::with_capacity(cap),
2069 insert_values: Vec::with_capacity(cap),
2070 insert_pushes: Vec::with_capacity(cap),
2071 returning_cols: Vec::new(),
2072 auto_assigns: Vec::new(),
2073 auto_field_idents: Vec::new(),
2074 first_auto_value_ty: None,
2075 bulk_pushes_no_auto: Vec::with_capacity(cap),
2076 bulk_pushes_all: Vec::with_capacity(cap),
2077 bulk_columns_no_auto: Vec::with_capacity(cap),
2078 bulk_columns_all: Vec::with_capacity(cap),
2079 bulk_auto_uniformity: Vec::new(),
2080 first_auto_ident: None,
2081 has_auto: false,
2082 pk_is_auto: false,
2083 update_assignments: Vec::with_capacity(cap),
2084 upsert_update_columns: Vec::with_capacity(cap),
2085 primary_key: None,
2086 column_entries: Vec::with_capacity(cap),
2087 field_names: Vec::with_capacity(cap),
2088 fk_relations: Vec::new(),
2089 soft_delete_column: None,
2090 soft_delete_field_ident: None,
2091 };
2092
2093 for field in &named.named {
2094 let info = process_field(field, table)?;
2095 out.field_names.push(info.ident.to_string());
2096 out.field_schemas.push(info.schema);
2097 out.from_row_inits.push(info.from_row_init);
2098 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2099 if let Some(parent_ty) = info.fk_inner.clone() {
2100 out.fk_relations.push(FkRelation {
2101 parent_type: parent_ty,
2102 fk_column: info.column.clone(),
2103 pk_kind: info.fk_pk_kind,
2104 nullable: info.nullable,
2105 related_name: info.related_name.clone(),
2106 });
2107 }
2108 if info.soft_delete {
2109 if out.soft_delete_column.is_some() {
2110 return Err(syn::Error::new_spanned(
2111 field,
2112 "only one field may be marked `#[rustango(soft_delete)]`",
2113 ));
2114 }
2115 out.soft_delete_column = Some(info.column.clone());
2116 out.soft_delete_field_ident = Some(info.ident.clone());
2117 }
2118 let column = info.column.as_str();
2119 let ident = info.ident;
2120 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2121 // skip every write path — Postgres recomputes the value
2122 // from EXPR. Push only the column-entry record (so typed
2123 // column constants still exist for filtering / projection)
2124 // and the schema literal (already pushed above) and move
2125 // on. No insert_columns/values, no insert_pushes, no
2126 // bulk_*, no update_assignments, no upsert_update_columns,
2127 // no returning_cols.
2128 if info.generated_as.is_some() {
2129 out.column_entries.push(ColumnEntry {
2130 ident: ident.clone(),
2131 value_ty: info.value_ty.clone(),
2132 name: ident.to_string(),
2133 column: info.column.clone(),
2134 field_type_tokens: info.field_type_tokens,
2135 });
2136 continue;
2137 }
2138 out.insert_columns.push(quote!(#column));
2139 out.insert_values.push(quote! {
2140 ::core::convert::Into::<#root::core::SqlValue>::into(
2141 ::core::clone::Clone::clone(&self.#ident)
2142 )
2143 });
2144 if info.auto {
2145 out.has_auto = true;
2146 if out.first_auto_ident.is_none() {
2147 out.first_auto_ident = Some(ident.clone());
2148 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2149 }
2150 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2151 // before binding, so the value is already in
2152 // `self.#ident` after the insert_push — RETURNING is
2153 // redundant. Skip adding this column to returning_cols /
2154 // auto_assigns / auto_field_idents to avoid (a) an
2155 // unnecessary RETURNING column on every dialect, and (b)
2156 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2157 // integer PK.
2158 if !info.default_uuid_v7 {
2159 out.returning_cols.push(quote!(#column));
2160 out.auto_field_idents
2161 .push((ident.clone(), info.column.clone()));
2162 out.auto_assigns.push(quote! {
2163 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2164 });
2165 }
2166 if info.default_uuid_v7 {
2167 // Rust-side UUIDv7 generation (issue #823, Eloquent
2168 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2169 // and bind; Auto::Set → bind the user's value. The
2170 // column is ALWAYS present in the INSERT statement —
2171 // no RETURNING / no DB DEFAULT needed.
2172 out.insert_pushes.push(quote! {
2173 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2174 self.#ident = #root::sql::Auto::Set(
2175 #root::__uuid::Uuid::now_v7(),
2176 );
2177 }
2178 if let #root::sql::Auto::Set(_v) = &self.#ident {
2179 _columns.push(#column);
2180 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2181 ::core::clone::Clone::clone(_v)
2182 ));
2183 }
2184 });
2185 } else {
2186 out.insert_pushes.push(quote! {
2187 if let #root::sql::Auto::Set(_v) = &self.#ident {
2188 _columns.push(#column);
2189 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2190 ::core::clone::Clone::clone(_v)
2191 ));
2192 }
2193 });
2194 }
2195 // Bulk: Auto fields appear only in the all-Set path,
2196 // never in the Unset path (we drop them from `columns`).
2197 out.bulk_columns_all.push(quote!(#column));
2198 out.bulk_pushes_all.push(quote! {
2199 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2200 ::core::clone::Clone::clone(&_row.#ident)
2201 ));
2202 });
2203 // Uniformity check: every row's Auto state must match the
2204 // first row's. Mixed Set/Unset within one bulk_insert is
2205 // rejected here so the column list stays consistent.
2206 let ident_clone = ident.clone();
2207 out.bulk_auto_uniformity.push(quote! {
2208 for _r in rows.iter().skip(1) {
2209 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2210 return ::core::result::Result::Err(
2211 #root::sql::ExecError::Sql(
2212 #root::sql::SqlError::BulkAutoMixed
2213 )
2214 );
2215 }
2216 }
2217 });
2218 } else {
2219 out.insert_pushes.push(quote! {
2220 _columns.push(#column);
2221 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2222 ::core::clone::Clone::clone(&self.#ident)
2223 ));
2224 });
2225 // Bulk: non-Auto fields appear in BOTH paths.
2226 out.bulk_columns_no_auto.push(quote!(#column));
2227 out.bulk_columns_all.push(quote!(#column));
2228 let push_expr = quote! {
2229 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2230 ::core::clone::Clone::clone(&_row.#ident)
2231 ));
2232 };
2233 out.bulk_pushes_no_auto.push(push_expr.clone());
2234 out.bulk_pushes_all.push(push_expr);
2235 }
2236 if info.primary_key {
2237 if out.primary_key.is_some() {
2238 return Err(syn::Error::new_spanned(
2239 field,
2240 "only one field may be marked `#[rustango(primary_key)]`",
2241 ));
2242 }
2243 out.primary_key = Some((ident.clone(), info.column.clone()));
2244 if info.auto {
2245 out.pk_is_auto = true;
2246 }
2247 } else if info.auto_now_add {
2248 // Immutable post-insert: skip from UPDATE entirely.
2249 } else if info.auto_now {
2250 // `auto_now` columns: bind `chrono::Utc::now()` on every
2251 // UPDATE so the column is always overridden with the
2252 // wall-clock at write time, regardless of what value the
2253 // user left in the struct field.
2254 out.update_assignments.push(quote! {
2255 #root::core::Assignment {
2256 column: #column,
2257 value: ::core::convert::Into::<#root::core::Expr>::into(
2258 ::core::convert::Into::<#root::core::SqlValue>::into(
2259 #root::__chrono::Utc::now()
2260 )
2261 ),
2262 }
2263 });
2264 out.upsert_update_columns.push(quote!(#column));
2265 } else {
2266 out.update_assignments.push(quote! {
2267 #root::core::Assignment {
2268 column: #column,
2269 value: ::core::convert::Into::<#root::core::Expr>::into(
2270 ::core::convert::Into::<#root::core::SqlValue>::into(
2271 ::core::clone::Clone::clone(&self.#ident)
2272 )
2273 ),
2274 }
2275 });
2276 out.upsert_update_columns.push(quote!(#column));
2277 }
2278 out.column_entries.push(ColumnEntry {
2279 ident: ident.clone(),
2280 value_ty: info.value_ty.clone(),
2281 name: ident.to_string(),
2282 column: info.column.clone(),
2283 field_type_tokens: info.field_type_tokens,
2284 });
2285 }
2286 Ok(out)
2287}
2288
2289fn model_impl_tokens(
2290 struct_name: &syn::Ident,
2291 model_name: &str,
2292 table: &str,
2293 display: Option<&str>,
2294 app_label: Option<&str>,
2295 admin: Option<&AdminAttrs>,
2296 default_order: &[(String, bool, proc_macro2::Span)],
2297 field_schemas: &[TokenStream2],
2298 soft_delete_column: Option<&str>,
2299 permissions: bool,
2300 audit_track: Option<&[String]>,
2301 m2m_relations: &[M2MAttr],
2302 indexes: &[IndexAttr],
2303 checks: &[CheckAttr],
2304 excludes: &[ExcludeAttr],
2305 composite_fks: &[CompositeFkAttr],
2306 generic_fks: &[GenericFkAttr],
2307 scope: Option<&str>,
2308 is_view: bool,
2309 verbose_name: Option<&str>,
2310 verbose_name_plural: Option<&str>,
2311 managed: bool,
2312 base_manager_name: Option<&str>,
2313 order_with_respect_to: Option<&str>,
2314 proxy: bool,
2315 required_db_features: &[String],
2316 required_db_vendor: Option<&str>,
2317 default_related_name: Option<&str>,
2318 db_table_comment: Option<&str>,
2319 get_latest_by: Option<(&str, bool)>,
2320 extra_permissions: &[(String, String)],
2321 default_permissions: &[String],
2322 global_scopes: &[GlobalScopeAttr],
2323 reverse_has_relations: &[ReverseHasAttr],
2324 generic_has_relations: &[GenericHasAttr],
2325) -> TokenStream2 {
2326 let root = rustango_root();
2327 let display_tokens = if let Some(name) = display {
2328 quote!(::core::option::Option::Some(#name))
2329 } else {
2330 quote!(::core::option::Option::None)
2331 };
2332 let app_label_tokens = if let Some(name) = app_label {
2333 quote!(::core::option::Option::Some(#name))
2334 } else {
2335 quote!(::core::option::Option::None)
2336 };
2337 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2338 quote!(::core::option::Option::Some(#col))
2339 } else {
2340 quote!(::core::option::Option::None)
2341 };
2342 let audit_track_tokens = match audit_track {
2343 None => quote!(::core::option::Option::None),
2344 Some(names) => {
2345 let lits = names.iter().map(|n| n.as_str());
2346 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2347 }
2348 };
2349 let admin_tokens = admin_config_tokens(admin);
2350 // Default `tenant` so single-tenant projects (no `scope` attr
2351 // anywhere) keep the v0.24.x behavior. Container-attr parser
2352 // already validated the value is "registry" or "tenant".
2353 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2354 Some("registry") => quote!(#root::core::ModelScope::Registry),
2355 _ => quote!(#root::core::ModelScope::Tenant),
2356 };
2357 let verbose_name_tokens = optional_str(verbose_name);
2358 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2359 let base_manager_name_tokens = optional_str(base_manager_name);
2360 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2361 let required_db_features_lits: Vec<&str> =
2362 required_db_features.iter().map(String::as_str).collect();
2363 let required_db_vendor_tokens = optional_str(required_db_vendor);
2364 let default_related_name_tokens = optional_str(default_related_name);
2365 let db_table_comment_tokens = optional_str(db_table_comment);
2366 let get_latest_by_tokens = match get_latest_by {
2367 Some((col, desc)) => {
2368 quote!(::core::option::Option::Some((#col, #desc)))
2369 }
2370 None => quote!(::core::option::Option::None),
2371 };
2372 let extra_permission_tokens: Vec<_> = extra_permissions
2373 .iter()
2374 .map(|(c, l)| quote!((#c, #l)))
2375 .collect();
2376 let default_permission_tokens: Vec<_> = default_permissions
2377 .iter()
2378 .map(|action| quote!(#action))
2379 .collect();
2380 let indexes_tokens = indexes.iter().map(|idx| {
2381 let name = idx.name.as_deref().unwrap_or("unnamed_index");
2382 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2383 let unique = idx.unique;
2384 // Map the parsed method string onto the IndexMethod enum
2385 // variant — kept at the codegen layer so the IR doesn't
2386 // carry the string form.
2387 let method_variant = match idx.method.as_str() {
2388 "gin" => quote!(#root::core::IndexMethod::Gin),
2389 "gist" => quote!(#root::core::IndexMethod::Gist),
2390 "brin" => quote!(#root::core::IndexMethod::Brin),
2391 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2392 "hash" => quote!(#root::core::IndexMethod::Hash),
2393 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2394 _ => quote!(#root::core::IndexMethod::BTree),
2395 };
2396 let where_clause = match &idx.where_clause {
2397 Some(s) => quote!(::core::option::Option::Some(#s)),
2398 None => quote!(::core::option::Option::None),
2399 };
2400 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2401 quote! {
2402 #root::core::IndexSchema {
2403 name: #name,
2404 columns: &[ #(#cols),* ],
2405 unique: #unique,
2406 method: #method_variant,
2407 where_clause: #where_clause,
2408 include: &[ #(#include_lits),* ],
2409 }
2410 }
2411 });
2412 let checks_tokens = checks.iter().map(|c| {
2413 let name = c.name.as_str();
2414 let expr = c.expr.as_str();
2415 quote! {
2416 #root::core::CheckConstraint {
2417 name: #name,
2418 expr: #expr,
2419 }
2420 }
2421 });
2422 let excludes_tokens = excludes.iter().map(|e| {
2423 let name = e.name.as_str();
2424 let using = e.using.as_str();
2425 let element_tokens = e.elements.iter().map(|(col, op)| {
2426 let col_s = col.as_str();
2427 let op_s = op.as_str();
2428 quote!((#col_s, #op_s))
2429 });
2430 let where_tokens = match e.where_clause.as_deref() {
2431 Some(w) => quote!(::core::option::Option::Some(#w)),
2432 None => quote!(::core::option::Option::None),
2433 };
2434 quote! {
2435 #root::core::ExclusionConstraint {
2436 name: #name,
2437 using: #using,
2438 elements: &[ #(#element_tokens),* ],
2439 where_clause: #where_tokens,
2440 }
2441 }
2442 });
2443 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2444 let name = rel.name.as_str();
2445 let to = rel.to.as_str();
2446 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2447 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2448 quote! {
2449 #root::core::CompositeFkRelation {
2450 name: #name,
2451 to: #to,
2452 from: &[ #(#from_cols),* ],
2453 on: &[ #(#on_cols),* ],
2454 }
2455 }
2456 });
2457 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2458 let name = rel.name.as_str();
2459 let ct_col = rel.ct_column.as_str();
2460 let pk_col = rel.pk_column.as_str();
2461 quote! {
2462 #root::core::GenericRelation {
2463 name: #name,
2464 ct_column: #ct_col,
2465 pk_column: #pk_col,
2466 }
2467 }
2468 });
2469 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2470 // no `#[rustango(default_order = "...")]` attribute was supplied.
2471 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2472 let col_lit = col.as_str();
2473 quote! { (#col_lit, #desc) }
2474 });
2475
2476 // Issue #820 — `global_scopes` slice literal. Empty when no
2477 // `#[rustango(global_scope(...))]` attribute was supplied. The
2478 // `apply` path is re-emitted verbatim so it resolves in the
2479 // consumer's scope; the name is stored as a string literal.
2480 let global_scope_tokens = global_scopes.iter().map(|s| {
2481 let name = s.name.as_str();
2482 let apply = &s.apply;
2483 quote! {
2484 #root::core::GlobalScope {
2485 name: #name,
2486 apply: #apply,
2487 }
2488 }
2489 });
2490
2491 let m2m_tokens = m2m_relations.iter().map(|rel| {
2492 let name = rel.name.as_str();
2493 let to = rel.to.as_str();
2494 let through = rel.through.as_str();
2495 let src = rel.src.as_str();
2496 let dst = rel.dst.as_str();
2497 let auto_create = rel.auto_create;
2498 quote! {
2499 #root::core::M2MRelation {
2500 name: #name,
2501 to: #to,
2502 through: #through,
2503 src_col: #src,
2504 dst_col: #dst,
2505 auto_create: #auto_create,
2506 }
2507 }
2508 });
2509 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2510 // when the model declares `#[rustango(reverse_has(...))]`. Each
2511 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2512 // a const expression. Models without reverse_has fall through to
2513 // the trait's empty default — no override emitted.
2514 let reverse_relations_override = if reverse_has_relations.is_empty() {
2515 quote!()
2516 } else {
2517 let entries = reverse_has_relations.iter().map(|rel| {
2518 let name = rel.name.as_str();
2519 let child = &rel.child;
2520 let child_fk_column = rel.child_fk_column.as_str();
2521 let self_pk_column = rel.self_pk_column.as_str();
2522 quote! {
2523 #root::core::ReverseRelation {
2524 name: #name,
2525 child_schema: <#child as #root::core::Model>::SCHEMA,
2526 child_fk_column: #child_fk_column,
2527 self_pk_column: #self_pk_column,
2528 }
2529 }
2530 });
2531 quote! {
2532 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2533 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2534 RELS
2535 }
2536 }
2537 };
2538 // Issue #830 — `Model::generic_reverse_relations()` override for
2539 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2540 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2541 quote!()
2542 } else {
2543 let entries = generic_has_relations.iter().map(|rel| {
2544 let name = rel.name.as_str();
2545 let child = &rel.child;
2546 let ct_column = rel.ct_column.as_str();
2547 let pk_column = rel.pk_column.as_str();
2548 let self_pk_column = rel.self_pk_column.as_str();
2549 quote! {
2550 #root::core::GenericReverseRelation {
2551 name: #name,
2552 child_schema: <#child as #root::core::Model>::SCHEMA,
2553 ct_column: #ct_column,
2554 pk_column: #pk_column,
2555 self_pk_column: #self_pk_column,
2556 }
2557 }
2558 });
2559 quote! {
2560 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2561 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2562 RELS
2563 }
2564 }
2565 };
2566 quote! {
2567 impl #root::core::Model for #struct_name {
2568 const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2569 name: #model_name,
2570 table: #table,
2571 fields: &[ #(#field_schemas),* ],
2572 display: #display_tokens,
2573 app_label: #app_label_tokens,
2574 admin: #admin_tokens,
2575 soft_delete_column: #soft_delete_tokens,
2576 permissions: #permissions,
2577 audit_track: #audit_track_tokens,
2578 m2m: &[ #(#m2m_tokens),* ],
2579 indexes: &[ #(#indexes_tokens),* ],
2580 check_constraints: &[ #(#checks_tokens),* ],
2581 exclusion_constraints: &[ #(#excludes_tokens),* ],
2582 composite_relations: &[ #(#composite_fk_tokens),* ],
2583 generic_relations: &[ #(#generic_fk_tokens),* ],
2584 scope: #scope_tokens,
2585 default_order: &[ #(#default_order_tokens),* ],
2586 is_view: #is_view,
2587 verbose_name: #verbose_name_tokens,
2588 verbose_name_plural: #verbose_name_plural_tokens,
2589 managed: #managed,
2590 base_manager_name: #base_manager_name_tokens,
2591 order_with_respect_to: #order_with_respect_to_tokens,
2592 proxy: #proxy,
2593 required_db_features: &[ #(#required_db_features_lits),* ],
2594 required_db_vendor: #required_db_vendor_tokens,
2595 default_related_name: #default_related_name_tokens,
2596 db_table_comment: #db_table_comment_tokens,
2597 get_latest_by: #get_latest_by_tokens,
2598 extra_permissions: &[ #(#extra_permission_tokens),* ],
2599 default_permissions: &[ #(#default_permission_tokens),* ],
2600 global_scopes: &[ #(#global_scope_tokens),* ],
2601 };
2602
2603 #reverse_relations_override
2604 #generic_reverse_relations_override
2605 }
2606 }
2607}
2608
2609/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2610/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2611/// otherwise a static reference to a populated `AdminConfig`.
2612fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2613 let root = rustango_root();
2614 let Some(admin) = admin else {
2615 return quote!(::core::option::Option::None);
2616 };
2617
2618 let list_display = admin
2619 .list_display
2620 .as_ref()
2621 .map(|(v, _)| v.as_slice())
2622 .unwrap_or(&[]);
2623 let list_display_lits = list_display.iter().map(|s| s.as_str());
2624
2625 let search_fields = admin
2626 .search_fields
2627 .as_ref()
2628 .map(|(v, _)| v.as_slice())
2629 .unwrap_or(&[]);
2630 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2631
2632 let readonly_fields = admin
2633 .readonly_fields
2634 .as_ref()
2635 .map(|(v, _)| v.as_slice())
2636 .unwrap_or(&[]);
2637 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2638
2639 let list_filter = admin
2640 .list_filter
2641 .as_ref()
2642 .map(|(v, _)| v.as_slice())
2643 .unwrap_or(&[]);
2644 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2645
2646 let actions = admin
2647 .actions
2648 .as_ref()
2649 .map(|(v, _)| v.as_slice())
2650 .unwrap_or(&[]);
2651 let actions_lits = actions.iter().map(|s| s.as_str());
2652
2653 let fieldsets = admin
2654 .fieldsets
2655 .as_ref()
2656 .map(|(v, _)| v.as_slice())
2657 .unwrap_or(&[]);
2658 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2659 let title = title.as_str();
2660 let field_lits = fields.iter().map(|s| s.as_str());
2661 quote!(#root::core::Fieldset {
2662 title: #title,
2663 fields: &[ #( #field_lits ),* ],
2664 })
2665 });
2666
2667 let list_display_links = admin
2668 .list_display_links
2669 .as_ref()
2670 .map(|(v, _)| v.as_slice())
2671 .unwrap_or(&[]);
2672 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2673
2674 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2675 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2676 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2677 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2678
2679 let prepopulated = admin
2680 .prepopulated_fields
2681 .as_ref()
2682 .map(|(v, _)| v.as_slice())
2683 .unwrap_or(&[]);
2684 let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2685 let target = target.as_str();
2686 let source_lits = sources.iter().map(|s| s.as_str());
2687 quote!(#root::core::PrepopulatedField {
2688 target: #target,
2689 sources: &[ #( #source_lits ),* ],
2690 })
2691 });
2692
2693 let raw_id_fields = admin
2694 .raw_id_fields
2695 .as_ref()
2696 .map(|(v, _)| v.as_slice())
2697 .unwrap_or(&[]);
2698 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2699
2700 let autocomplete_fields = admin
2701 .autocomplete_fields
2702 .as_ref()
2703 .map(|(v, _)| v.as_slice())
2704 .unwrap_or(&[]);
2705 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2706
2707 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2708 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2709 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2710 Some("none") => quote!(#root::core::ListSelectRelated::None),
2711 Some(raw) => {
2712 let names: Vec<&str> = raw
2713 .split(',')
2714 .map(str::trim)
2715 .filter(|s| !s.is_empty())
2716 .collect();
2717 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2718 }
2719 };
2720
2721 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2722 // a Vec<(String, String)>. Empty / unset → no overrides.
2723 let formfield_pairs: Vec<(&str, &str)> = admin
2724 .formfield_overrides
2725 .as_ref()
2726 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2727 .unwrap_or_default();
2728 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2729 let field = *field;
2730 let widget = *widget;
2731 quote!((#field, #widget))
2732 });
2733
2734 let list_per_page = admin.list_per_page.unwrap_or(0);
2735
2736 let ordering_pairs = admin
2737 .ordering
2738 .as_ref()
2739 .map(|(v, _)| v.as_slice())
2740 .unwrap_or(&[]);
2741 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2742 let name = name.as_str();
2743 let desc = *desc;
2744 quote!((#name, #desc))
2745 });
2746
2747 quote! {
2748 ::core::option::Option::Some(&#root::core::AdminConfig {
2749 list_display: &[ #( #list_display_lits ),* ],
2750 search_fields: &[ #( #search_fields_lits ),* ],
2751 list_per_page: #list_per_page,
2752 ordering: &[ #( #ordering_tokens ),* ],
2753 readonly_fields: &[ #( #readonly_fields_lits ),* ],
2754 list_filter: &[ #( #list_filter_lits ),* ],
2755 actions: &[ #( #actions_lits ),* ],
2756 fieldsets: &[ #( #fieldset_tokens ),* ],
2757 list_display_links: &[ #( #list_display_links_lits ),* ],
2758 search_help_text: #search_help_text,
2759 actions_on_top: #actions_on_top,
2760 actions_on_bottom: #actions_on_bottom,
2761 date_hierarchy: #date_hierarchy,
2762 prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2763 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2764 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2765 list_select_related: #list_select_related_tokens,
2766 formfield_overrides: &[ #( #formfield_tokens ),* ],
2767 })
2768 }
2769}
2770
2771fn inherent_impl_tokens(
2772 struct_name: &syn::Ident,
2773 fields: &CollectedFields,
2774 primary_key: Option<&(syn::Ident, String)>,
2775 column_consts: &TokenStream2,
2776 audited_fields: Option<&[&ColumnEntry]>,
2777 indexes: &[IndexAttr],
2778 manager_fns: &[syn::Ident],
2779) -> TokenStream2 {
2780 let root = rustango_root();
2781 // Audit-emit fragments threaded into write paths. Non-empty only
2782 // when the model carries `#[rustango(audit(...))]`. They reborrow
2783 // `_executor` (a `&mut PgConnection` for audited models — the
2784 // macro switches the signature below) so the data write and the
2785 // audit INSERT both run on the same caller-supplied connection.
2786 let executor_passes_to_data_write = if audited_fields.is_some() {
2787 quote!(&mut *_executor)
2788 } else {
2789 quote!(_executor)
2790 };
2791 let executor_param = if audited_fields.is_some() {
2792 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2793 } else {
2794 quote!(_executor: _E)
2795 };
2796 let executor_generics = if audited_fields.is_some() {
2797 quote!()
2798 } else {
2799 quote!(<'_c, _E>)
2800 };
2801 let executor_where = if audited_fields.is_some() {
2802 quote!()
2803 } else {
2804 quote! {
2805 where
2806 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2807 }
2808 };
2809 // For audited models the `_on` methods take `&mut PgConnection`, so
2810 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2811 // must acquire a connection first. Non-audited models keep the
2812 // direct delegation since `&PgPool` IS an Executor.
2813 let pool_to_save_on = if audited_fields.is_some() {
2814 quote! {
2815 let mut _conn = pool.acquire().await?;
2816 self.save_on(&mut *_conn).await
2817 }
2818 } else {
2819 quote!(self.save_on(pool).await)
2820 };
2821 let pool_to_insert_on = if audited_fields.is_some() {
2822 quote! {
2823 let mut _conn = pool.acquire().await?;
2824 self.insert_on(&mut *_conn).await
2825 }
2826 } else {
2827 quote!(self.insert_on(pool).await)
2828 };
2829 let pool_to_delete_on = if audited_fields.is_some() {
2830 quote! {
2831 let mut _conn = pool.acquire().await?;
2832 self.delete_on(&mut *_conn).await
2833 }
2834 } else {
2835 quote!(self.delete_on(pool).await)
2836 };
2837 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2838 quote! {
2839 let mut _conn = pool.acquire().await?;
2840 Self::bulk_insert_on(rows, &mut *_conn).await
2841 }
2842 } else {
2843 quote!(Self::bulk_insert_on(rows, pool).await)
2844 };
2845 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2846 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2847 // directly, but `upsert_on` for audited models takes
2848 // `&mut PgConnection` (the audit emit needs a real connection).
2849 // Add the missing acquire shim to keep audited Auto-PK upsert
2850 // compiling.
2851 let pool_to_upsert_on = if audited_fields.is_some() {
2852 quote! {
2853 let mut _conn = pool.acquire().await?;
2854 self.upsert_on(&mut *_conn).await
2855 }
2856 } else {
2857 quote!(self.upsert_on(pool).await)
2858 };
2859
2860 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2861 // (audit-on-connection over &Pool needs a bi-dialect transaction
2862 // helper, deferred). Two body shapes:
2863 // - has_auto: build InsertQuery skipping Auto::Unset columns,
2864 // request Auto cols in `returning`, dispatch via
2865 // `insert_returning_pool`, then on the returned `PgRow` /
2866 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
2867 // row OR drop the single i64 into the first Auto field on MySQL
2868 // (multi-Auto models on MySQL error at runtime since
2869 // `LAST_INSERT_ID()` only reports one)
2870 // - non-Auto: build InsertQuery with explicit columns/values and
2871 // call `insert_pool` (no returning needed)
2872 // pool_insert_method body for the audited Auto-PK case is moved
2873 // to after audit_pair_tokens / audit_pk_to_string (they live
2874 // ~150 lines below). This block keeps the non-audited and
2875 // non-Auto branches in place — the audited Auto-PK arm is
2876 // computed below and merged via the dispatch helper variable.
2877 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2878 // Audited models with explicit (non-Auto) PKs go through
2879 // the non-Auto insert path below — the audit emit is one
2880 // round-trip after the INSERT inside the same tx via
2881 // audit::save_one_with_audit? No, INSERT semantics
2882 // differ. For non-Auto PK + audited, route through a
2883 // dedicated insert + audit emit on the same tx, but defer
2884 // the macro emission to the audit-bundle-aware block below
2885 // — this `quote!()` placeholder gets overwritten there.
2886 quote!()
2887 } else if audited_fields.is_some() && fields.has_auto {
2888 // Audited Auto-PK insert_pool — assembled after the audit
2889 // bundles. Placeholder; real emission below.
2890 quote!()
2891 } else if fields.has_auto {
2892 let pushes = &fields.insert_pushes;
2893 let returning_cols = &fields.returning_cols;
2894 // When every `Auto<T>` field is filled Rust-side
2895 // (`default_uuid_v7`, issue #823), there is no column to read
2896 // back from the database — `returning_cols` is empty. Route
2897 // through plain `insert_pool` instead of
2898 // `insert_returning_pool` to skip the redundant RETURNING /
2899 // LAST_INSERT_ID round-trip.
2900 if fields.returning_cols.is_empty() {
2901 quote! {
2902 /// Insert this row against either backend. Every
2903 /// `Auto<T>` PK on this model is filled Rust-side
2904 /// (e.g. `default_uuid_v7`) before binding, so no
2905 /// RETURNING round-trip is needed.
2906 ///
2907 /// # Errors
2908 /// As [`Self::insert`].
2909 pub async fn insert_pool(
2910 &mut self,
2911 pool: &#root::sql::Pool,
2912 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2913 let mut _columns: ::std::vec::Vec<&'static str> =
2914 ::std::vec::Vec::new();
2915 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2916 ::std::vec::Vec::new();
2917 #( #pushes )*
2918 let _query = #root::core::InsertQuery {
2919 model: <Self as #root::core::Model>::SCHEMA,
2920 columns: _columns,
2921 values: _values,
2922 returning: ::std::vec::Vec::new(),
2923 on_conflict: ::core::option::Option::None,
2924 };
2925 #root::sql::insert_pool(pool, &_query).await
2926 }
2927
2928 /// Eloquent `Model::insertOrIgnore()` — same shape
2929 /// as the auto-PK branch above. Returns `Ok(true)`
2930 /// when inserted, `Ok(false)` when a conflict caused
2931 /// the INSERT to silently skip.
2932 ///
2933 /// # Errors
2934 /// As [`Self::insert`].
2935 pub async fn insert_or_ignore(
2936 &mut self,
2937 pool: &#root::sql::Pool,
2938 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2939 let mut _columns: ::std::vec::Vec<&'static str> =
2940 ::std::vec::Vec::new();
2941 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2942 ::std::vec::Vec::new();
2943 #( #pushes )*
2944 let _query = #root::core::InsertQuery {
2945 model: <Self as #root::core::Model>::SCHEMA,
2946 columns: _columns,
2947 values: _values,
2948 returning: ::std::vec::Vec::new(),
2949 on_conflict: ::core::option::Option::Some(
2950 #root::core::ConflictClause::DoNothing,
2951 ),
2952 };
2953 let dialect = pool.dialect();
2954 let stmt = dialect.compile_insert(&_query)?;
2955 let rows = #root::sql::raw_execute_pool(
2956 pool, &stmt.sql, stmt.params,
2957 ).await?;
2958 ::core::result::Result::Ok(rows > 0)
2959 }
2960 }
2961 } else {
2962 quote! {
2963 /// Insert this row against either backend, populating any
2964 /// `Auto<T>` PK from the auto-assigned value.
2965 ///
2966 /// # Errors
2967 /// As [`Self::insert`].
2968 pub async fn insert_pool(
2969 &mut self,
2970 pool: &#root::sql::Pool,
2971 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2972 let mut _columns: ::std::vec::Vec<&'static str> =
2973 ::std::vec::Vec::new();
2974 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2975 ::std::vec::Vec::new();
2976 #( #pushes )*
2977 let _query = #root::core::InsertQuery {
2978 model: <Self as #root::core::Model>::SCHEMA,
2979 columns: _columns,
2980 values: _values,
2981 returning: ::std::vec![ #( #returning_cols ),* ],
2982 on_conflict: ::core::option::Option::None,
2983 };
2984 let _result = #root::sql::insert_returning_pool(
2985 pool, &_query,
2986 ).await?;
2987 #root::sql::apply_auto_pk(_result, self)
2988 }
2989
2990 /// Eloquent `Model::insertOrIgnore()` — INSERT this
2991 /// row or silently skip on unique-constraint
2992 /// violation. Returns `Ok(true)` when a row was
2993 /// inserted, `Ok(false)` when a conflict caused the
2994 /// INSERT to silently skip.
2995 ///
2996 /// **Caveat on auto-PK models**: when the row is
2997 /// skipped (conflict), this instance's `Auto<T>`
2998 /// fields stay `Unset` — no PK is back-populated
2999 /// because the server didn't auto-assign one. For
3000 /// "insert then read back the PK or the existing
3001 /// row's PK", use the `upsert` family or
3002 /// `get_or_create`.
3003 ///
3004 /// # Errors
3005 /// As [`Self::insert`].
3006 pub async fn insert_or_ignore(
3007 &mut self,
3008 pool: &#root::sql::Pool,
3009 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3010 let mut _columns: ::std::vec::Vec<&'static str> =
3011 ::std::vec::Vec::new();
3012 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3013 ::std::vec::Vec::new();
3014 #( #pushes )*
3015 let _query = #root::core::InsertQuery {
3016 model: <Self as #root::core::Model>::SCHEMA,
3017 columns: _columns,
3018 values: _values,
3019 returning: ::std::vec::Vec::new(),
3020 on_conflict: ::core::option::Option::Some(
3021 #root::core::ConflictClause::DoNothing,
3022 ),
3023 };
3024 let dialect = pool.dialect();
3025 let stmt = dialect.compile_insert(&_query)?;
3026 let rows = #root::sql::raw_execute_pool(
3027 pool, &stmt.sql, stmt.params,
3028 ).await?;
3029 ::core::result::Result::Ok(rows > 0)
3030 }
3031 }
3032 }
3033 } else {
3034 let insert_columns = &fields.insert_columns;
3035 let insert_values = &fields.insert_values;
3036 quote! {
3037 /// Insert this row into its table against either backend.
3038 /// Equivalent to [`Self::insert`] but takes
3039 /// [`#root::sql::Pool`].
3040 ///
3041 /// # Errors
3042 /// As [`Self::insert`].
3043 pub async fn insert_pool(
3044 &self,
3045 pool: &#root::sql::Pool,
3046 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3047 let _query = #root::core::InsertQuery {
3048 model: <Self as #root::core::Model>::SCHEMA,
3049 columns: ::std::vec![ #( #insert_columns ),* ],
3050 values: ::std::vec![ #( #insert_values ),* ],
3051 returning: ::std::vec::Vec::new(),
3052 on_conflict: ::core::option::Option::None,
3053 };
3054 #root::sql::insert_pool(pool, &_query).await
3055 }
3056
3057 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3058 /// or silently skip on unique-constraint violation. Maps
3059 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3060 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3061 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3062 /// `INSERT IGNORE INTO …`.
3063 ///
3064 /// Returns `Ok(true)` when a row was inserted,
3065 /// `Ok(false)` when a conflict caused the INSERT to
3066 /// silently skip.
3067 ///
3068 /// Use for "create-if-absent" patterns where you don't
3069 /// need the row back. For "find-or-create with the row
3070 /// returned", use the queryset-level
3071 /// `crate::sql::get_or_create` free function.
3072 ///
3073 /// # Errors
3074 /// As [`Self::insert`], plus any dialect-specific
3075 /// translation error from the ConflictClause writer.
3076 pub async fn insert_or_ignore(
3077 &self,
3078 pool: &#root::sql::Pool,
3079 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3080 let _query = #root::core::InsertQuery {
3081 model: <Self as #root::core::Model>::SCHEMA,
3082 columns: ::std::vec![ #( #insert_columns ),* ],
3083 values: ::std::vec![ #( #insert_values ),* ],
3084 returning: ::std::vec::Vec::new(),
3085 on_conflict: ::core::option::Option::Some(
3086 #root::core::ConflictClause::DoNothing,
3087 ),
3088 };
3089 let dialect = pool.dialect();
3090 let stmt = dialect.compile_insert(&_query)?;
3091 let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3092 ::core::result::Result::Ok(rows > 0)
3093 }
3094 }
3095 };
3096
3097 // pool_save_method moved to after audit_pair_tokens /
3098 // audit_pk_to_string (they live ~70 lines below) — needed for
3099 // the audited branch which builds an UpdateQuery + PendingEntry
3100 // and dispatches via audit::save_one_with_audit.
3101
3102 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3103 // are computed (they live ~80 lines below).
3104
3105 // Build the (column, JSON value) pair list used by every
3106 // snapshot-style audit emission. Reused across delete_on,
3107 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3108 // when the model isn't audited.
3109 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3110 .map(|tracked| {
3111 tracked
3112 .iter()
3113 .map(|c| {
3114 let column_lit = c.column.as_str();
3115 let ident = &c.ident;
3116 quote! {
3117 (
3118 #column_lit,
3119 #root::__serde_json::to_value(&self.#ident)
3120 .unwrap_or(#root::__serde_json::Value::Null),
3121 )
3122 }
3123 })
3124 .collect()
3125 })
3126 .unwrap_or_default();
3127 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3128 if fields.pk_is_auto {
3129 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3130 } else {
3131 quote!(::std::format!("{}", &self.#pk_ident))
3132 }
3133 } else {
3134 quote!(::std::string::String::new())
3135 };
3136 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3137 if audited_fields.is_some() {
3138 let pairs = audit_pair_tokens.iter();
3139 let pk_str = audit_pk_to_string.clone();
3140 quote! {
3141 let _audit_entry = #root::audit::PendingEntry {
3142 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3143 entity_pk: #pk_str,
3144 operation: #op_path,
3145 source: #root::audit::current_source(),
3146 changes: #root::audit::snapshot_changes(&[
3147 #( #pairs ),*
3148 ]),
3149 };
3150 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3151 }
3152 } else {
3153 quote!()
3154 }
3155 };
3156 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3157 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3158 let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3159 let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3160
3161 // `save_pool(&Pool)` — emitted for every model with a PK.
3162 // Audited Auto-PK models are deferred (the Auto::Unset →
3163 // insert_pool path needs the audited-insert flow from a future
3164 // batch). Three body shapes:
3165 // - non-audited, plain PK: build UpdateQuery + dispatch through
3166 // sql::update_pool
3167 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3168 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3169 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3170 // through audit::save_one_with_audit (per-backend tx wraps
3171 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3172 // write field values) — diff-style audit (with pre-UPDATE
3173 // SELECT for `before` values) needs per-tracked-column codegen
3174 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3175 // `save` keeps the diff for now.
3176 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3177 let pk_column_lit = pk_col.as_str();
3178 let assignments = &fields.update_assignments;
3179 if audited_fields.is_some() {
3180 if fields.pk_is_auto {
3181 // Auto-PK + audited: defer. The Auto::Unset insert
3182 // path needs a transactional INSERT + LAST_INSERT_ID
3183 // + audit emit flow — that's a follow-up batch.
3184 quote!()
3185 } else {
3186 let pairs = audit_pair_tokens.iter();
3187 let pairs2 = audit_pair_tokens.iter();
3188 let pk_str = audit_pk_to_string.clone();
3189 let pk_str2 = audit_pk_to_string.clone();
3190 quote! {
3191 /// Save (UPDATE) this row against either backend
3192 /// with audit emission inside the same transaction.
3193 /// Bi-dialect counterpart of [`Self::save`] for
3194 /// audited models with non-`Auto<T>` PKs.
3195 ///
3196 /// Captures **post-write** field state (snapshot
3197 /// audit). The legacy &PgPool [`Self::save`]
3198 /// captures BEFORE+AFTER for true diff audit;
3199 /// porting that to the &Pool path needs runtime
3200 /// per-tracked-column decoding and is deferred.
3201 ///
3202 /// # Errors
3203 /// As [`Self::save`].
3204 pub async fn save_pool(
3205 &mut self,
3206 pool: &#root::sql::Pool,
3207 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3208 let _query = #root::core::UpdateQuery {
3209 model: <Self as #root::core::Model>::SCHEMA,
3210 set: ::std::vec![ #( #assignments ),* ],
3211 where_clause: #root::core::WhereExpr::Predicate(
3212 #root::core::Filter {
3213 column: #pk_column_lit,
3214 op: #root::core::Op::Eq,
3215 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3216 ::core::clone::Clone::clone(&self.#pk_ident)
3217 ),
3218 }
3219 ),
3220 };
3221 let _audit_entry = #root::audit::PendingEntry {
3222 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3223 entity_pk: #pk_str,
3224 operation: #root::audit::AuditOp::Update,
3225 source: #root::audit::current_source(),
3226 changes: #root::audit::snapshot_changes(&[
3227 #( #pairs ),*
3228 ]),
3229 };
3230 let _ = #root::audit::save_one_with_audit(
3231 pool, &_query, &_audit_entry,
3232 ).await?;
3233 ::core::result::Result::Ok(())
3234 }
3235
3236 /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3237 /// (Django `Model.save(update_fields=[...])`).
3238 /// Audit emission shrinks to the same column set so
3239 /// the audit log reflects exactly what was written.
3240 ///
3241 /// # Errors
3242 /// As [`Self::save_pool`], plus
3243 /// [`#root::core::QueryError::UnknownField`] wrapped
3244 /// in `ExecError::Query` for unknown field names.
3245 pub async fn save_partial(
3246 &mut self,
3247 fields: &[&str],
3248 pool: &#root::sql::Pool,
3249 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3250 if fields.is_empty() {
3251 #root::__tracing::warn!(
3252 target: "rustango::save_partial",
3253 model = <Self as #root::core::Model>::SCHEMA.name,
3254 "save_partial called with empty field list — no-op"
3255 );
3256 return ::core::result::Result::Ok(());
3257 }
3258 let _schema = <Self as #root::core::Model>::SCHEMA;
3259 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3260 ::std::collections::HashSet::with_capacity(fields.len());
3261 for f in fields {
3262 match _schema.field(f) {
3263 ::core::option::Option::Some(fs) => {
3264 _wanted_cols.insert(fs.column);
3265 }
3266 ::core::option::Option::None => {
3267 return ::core::result::Result::Err(
3268 #root::sql::ExecError::Query(
3269 #root::core::QueryError::UnknownField {
3270 model: _schema.name,
3271 field: (*f).to_owned(),
3272 }
3273 )
3274 );
3275 }
3276 }
3277 }
3278 let _full: ::std::vec::Vec<#root::core::Assignment> =
3279 ::std::vec![ #( #assignments ),* ];
3280 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3281 .into_iter()
3282 .filter(|a| _wanted_cols.contains(a.column))
3283 .collect();
3284 if _filtered.is_empty() {
3285 #root::__tracing::warn!(
3286 target: "rustango::save_partial",
3287 model = _schema.name,
3288 "save_partial: every named field maps to a non-assignable column — no-op"
3289 );
3290 return ::core::result::Result::Ok(());
3291 }
3292 let _query = #root::core::UpdateQuery {
3293 model: _schema,
3294 set: _filtered,
3295 where_clause: #root::core::WhereExpr::Predicate(
3296 #root::core::Filter {
3297 column: #pk_column_lit,
3298 op: #root::core::Op::Eq,
3299 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3300 ::core::clone::Clone::clone(&self.#pk_ident)
3301 ),
3302 }
3303 ),
3304 };
3305 // Narrow the audit snapshot to the same column set.
3306 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3307 ::std::vec![ #( #pairs2 ),* ];
3308 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3309 _all_pairs
3310 .into_iter()
3311 .filter(|(col, _)| _wanted_cols.contains(col))
3312 .collect();
3313 let _audit_entry = #root::audit::PendingEntry {
3314 entity_table: _schema.table,
3315 entity_pk: #pk_str2,
3316 operation: #root::audit::AuditOp::Update,
3317 source: #root::audit::current_source(),
3318 changes: #root::audit::snapshot_changes(&_narrowed),
3319 };
3320 let _ = #root::audit::save_one_with_audit(
3321 pool, &_query, &_audit_entry,
3322 ).await?;
3323 ::core::result::Result::Ok(())
3324 }
3325
3326 /// Typed-column counterpart of [`Self::save_partial`] —
3327 /// issue #67. `fields` is a tuple of [`Column`]
3328 /// constants whose `Model` matches `Self`; typos and
3329 /// model mismatches surface at *compile time*
3330 /// (`Author::name` inside a `Post::save_partial_typed`
3331 /// call is a type error, no runtime check).
3332 ///
3333 /// ```ignore
3334 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3335 /// ```
3336 ///
3337 /// Lowers to [`Self::save_partial`] under the hood;
3338 /// audit narrowing + every other semantic is identical.
3339 ///
3340 /// [`Column`]: #root::core::Column
3341 ///
3342 /// # Errors
3343 /// As [`Self::save_partial`].
3344 pub async fn save_partial_typed<
3345 L: #root::core::TypedFieldList<Self>,
3346 >(
3347 &mut self,
3348 fields: L,
3349 pool: &#root::sql::Pool,
3350 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3351 let _names = fields.rust_field_names();
3352 let _refs: ::std::vec::Vec<&str> =
3353 _names.iter().copied().collect();
3354 self.save_partial(&_refs, pool).await
3355 }
3356 }
3357 }
3358 } else {
3359 let dispatch_unset = if fields.pk_is_auto {
3360 quote! {
3361 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3362 return self.insert_pool(pool).await;
3363 }
3364 }
3365 } else {
3366 quote!()
3367 };
3368 quote! {
3369 /// Save this row to its table against either backend.
3370 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3371 /// `UPDATE` keyed on the PK.
3372 ///
3373 /// # Errors
3374 /// As [`Self::save`].
3375 pub async fn save_pool(
3376 &mut self,
3377 pool: &#root::sql::Pool,
3378 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3379 #dispatch_unset
3380 let _query = #root::core::UpdateQuery {
3381 model: <Self as #root::core::Model>::SCHEMA,
3382 set: ::std::vec![ #( #assignments ),* ],
3383 where_clause: #root::core::WhereExpr::Predicate(
3384 #root::core::Filter {
3385 column: #pk_column_lit,
3386 op: #root::core::Op::Eq,
3387 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3388 ::core::clone::Clone::clone(&self.#pk_ident)
3389 ),
3390 }
3391 ),
3392 };
3393 let _ = #root::sql::update_pool(pool, &_query).await?;
3394 ::core::result::Result::Ok(())
3395 }
3396
3397 /// Save (UPDATE) only the listed Rust-side fields,
3398 /// leaving every other column untouched. Issue #66 —
3399 /// Django's `Model.save(update_fields=[...])` shape.
3400 ///
3401 /// `fields` are Rust-side struct field names; the macro
3402 /// resolves each to its SQL column. Unknown field
3403 /// names return [`#root::core::QueryError::UnknownField`]
3404 /// wrapped in `ExecError::Query`. An empty list is a
3405 /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3406 /// matching Django's "nothing to do" semantic.
3407 ///
3408 /// Use this when:
3409 /// * you only mutated a couple of fields on a wide row
3410 /// (avoid re-writing every column on every save), or
3411 /// * two writers diverged after their initial read and
3412 /// you want to preserve the other writer's changes to
3413 /// columns you didn't touch.
3414 ///
3415 /// Auto-PK models with an unset PK return
3416 /// [`#root::core::QueryError::UnknownField`] with
3417 /// field name `<pk>` — `save_partial` is an
3418 /// UPDATE-only path. Call [`Self::insert_pool`]
3419 /// (or [`Self::save_pool`] which dispatches based on
3420 /// PK state) for the INSERT case.
3421 ///
3422 /// # Errors
3423 /// As [`Self::save_pool`], plus `UnknownField` for
3424 /// unknown / empty / Auto-Unset cases.
3425 pub async fn save_partial(
3426 &mut self,
3427 fields: &[&str],
3428 pool: &#root::sql::Pool,
3429 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3430 if fields.is_empty() {
3431 #root::__tracing::warn!(
3432 target: "rustango::save_partial",
3433 model = <Self as #root::core::Model>::SCHEMA.name,
3434 "save_partial called with empty field list — no-op"
3435 );
3436 return ::core::result::Result::Ok(());
3437 }
3438 let _schema = <Self as #root::core::Model>::SCHEMA;
3439 // Validate field names against the schema.
3440 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3441 ::std::collections::HashSet::with_capacity(fields.len());
3442 for f in fields {
3443 match _schema.field(f) {
3444 ::core::option::Option::Some(fs) => {
3445 _wanted_cols.insert(fs.column);
3446 }
3447 ::core::option::Option::None => {
3448 return ::core::result::Result::Err(
3449 #root::sql::ExecError::Query(
3450 #root::core::QueryError::UnknownField {
3451 model: _schema.name,
3452 field: (*f).to_owned(),
3453 }
3454 )
3455 );
3456 }
3457 }
3458 }
3459 // Build the full assignment vec, then keep only the
3460 // assignments whose column is in `_wanted_cols`.
3461 let _full: ::std::vec::Vec<#root::core::Assignment> =
3462 ::std::vec![ #( #assignments ),* ];
3463 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3464 .into_iter()
3465 .filter(|a| _wanted_cols.contains(a.column))
3466 .collect();
3467 if _filtered.is_empty() {
3468 // All field names valid, but they all map to
3469 // non-assignable slots (PK column, computed/
3470 // virtual fields, relations without an
3471 // assignment). Same no-op semantic as Django.
3472 #root::__tracing::warn!(
3473 target: "rustango::save_partial",
3474 model = _schema.name,
3475 "save_partial: every named field maps to a non-assignable column — no-op"
3476 );
3477 return ::core::result::Result::Ok(());
3478 }
3479 let _query = #root::core::UpdateQuery {
3480 model: _schema,
3481 set: _filtered,
3482 where_clause: #root::core::WhereExpr::Predicate(
3483 #root::core::Filter {
3484 column: #pk_column_lit,
3485 op: #root::core::Op::Eq,
3486 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3487 ::core::clone::Clone::clone(&self.#pk_ident)
3488 ),
3489 }
3490 ),
3491 };
3492 let _ = #root::sql::update_pool(pool, &_query).await?;
3493 ::core::result::Result::Ok(())
3494 }
3495
3496 /// Typed-column counterpart of [`Self::save_partial`] —
3497 /// issue #67. `fields` is a tuple of [`Column`]
3498 /// constants whose `Model` matches `Self`; typos and
3499 /// model mismatches surface at *compile time*
3500 /// (`Author::name` inside a `Post::save_partial_typed`
3501 /// call is a type error, no runtime check).
3502 ///
3503 /// ```ignore
3504 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3505 /// ```
3506 ///
3507 /// Lowers to [`Self::save_partial`] under the hood — the
3508 /// tuple is reduced to a `&[&str]` slice of Rust-side
3509 /// field names and forwarded.
3510 ///
3511 /// [`Column`]: #root::core::Column
3512 ///
3513 /// # Errors
3514 /// As [`Self::save_partial`].
3515 pub async fn save_partial_typed<
3516 L: #root::core::TypedFieldList<Self>,
3517 >(
3518 &mut self,
3519 fields: L,
3520 pool: &#root::sql::Pool,
3521 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3522 let _names = fields.rust_field_names();
3523 let _refs: ::std::vec::Vec<&str> =
3524 _names.iter().copied().collect();
3525 self.save_partial(&_refs, pool).await
3526 }
3527 }
3528 }
3529 } else {
3530 quote!()
3531 };
3532
3533 // Audited `insert_pool` (overrides the placeholder set higher up
3534 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3535 // audited models get insert_pool routing through
3536 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3537 // + auto-PK readback + audit emit). Snapshot-style audit (the
3538 // PendingEntry's `changes` carries post-write field values).
3539 let pool_insert_method = if audited_fields.is_some() {
3540 if let Some(_) = primary_key {
3541 let pushes = if fields.has_auto {
3542 fields.insert_pushes.clone()
3543 } else {
3544 // For non-Auto-PK models, the macro normally builds
3545 // {columns, values} from fields.insert_columns +
3546 // fields.insert_values rather than insert_pushes.
3547 // Map those into the pushes shape.
3548 fields
3549 .insert_columns
3550 .iter()
3551 .zip(&fields.insert_values)
3552 .map(|(col, val)| {
3553 quote! {
3554 _columns.push(#col);
3555 _values.push(#val);
3556 }
3557 })
3558 .collect()
3559 };
3560 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3561 fields.returning_cols.clone()
3562 } else {
3563 // Non-Auto-PK: still need RETURNING something for the
3564 // audit helper's contract (it errors on empty
3565 // returning). Return the PK column so the audit row
3566 // can carry the assigned PK back. Some non-Auto PKs
3567 // are server-side-default (e.g. UUIDv4 default), so
3568 // RETURNING is genuinely useful.
3569 primary_key
3570 .map(|(_, col)| {
3571 let lit = col.as_str();
3572 vec![quote!(#lit)]
3573 })
3574 .unwrap_or_default()
3575 };
3576 let pairs = audit_pair_tokens.iter();
3577 let pk_str = audit_pk_to_string.clone();
3578 quote! {
3579 /// Insert this row against either backend with audit
3580 /// emission inside the same transaction. Bi-dialect
3581 /// counterpart of [`Self::insert`] for audited models.
3582 ///
3583 /// Snapshot-style audit (post-write field values).
3584 ///
3585 /// # Errors
3586 /// As [`Self::insert`].
3587 pub async fn insert_pool(
3588 &mut self,
3589 pool: &#root::sql::Pool,
3590 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3591 let mut _columns: ::std::vec::Vec<&'static str> =
3592 ::std::vec::Vec::new();
3593 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3594 ::std::vec::Vec::new();
3595 #( #pushes )*
3596 let _query = #root::core::InsertQuery {
3597 model: <Self as #root::core::Model>::SCHEMA,
3598 columns: _columns,
3599 values: _values,
3600 returning: ::std::vec![ #( #returning_cols ),* ],
3601 on_conflict: ::core::option::Option::None,
3602 };
3603 let _audit_entry = #root::audit::PendingEntry {
3604 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3605 entity_pk: #pk_str,
3606 operation: #root::audit::AuditOp::Create,
3607 source: #root::audit::current_source(),
3608 changes: #root::audit::snapshot_changes(&[
3609 #( #pairs ),*
3610 ]),
3611 };
3612 let _result = #root::audit::insert_one_with_audit(
3613 pool, &_query, &_audit_entry,
3614 ).await?;
3615 #root::sql::apply_auto_pk(_result, self)
3616 }
3617 }
3618 } else {
3619 quote!()
3620 }
3621 } else {
3622 // Keep the non-audited pool_insert_method we built earlier.
3623 pool_insert_method
3624 };
3625
3626 // Update audited save_pool: now that insert_pool is wired for
3627 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3628 // insert_pool. Non-audited save_pool already does this.
3629 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3630 // Replaces the snapshot-only emission with a per-backend transaction
3631 // body that:
3632 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3633 // column), capturing BEFORE values
3634 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3635 // 3. builds AFTER pairs from &self
3636 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3637 // AuditOp::Update + diff_changes(...) on the same tx connection
3638 // 5. commits
3639 //
3640 // Per-backend arms inline the SQL string + placeholder shape, then
3641 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3642 // is polymorphic over Row type — the same tokens work against
3643 // PgRow and MySqlRow as long as the field's Rust type implements
3644 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3645 // primitives + chrono/uuid/serde_json::Value all do).
3646 let pool_save_method = if let Some(tracked) = audited_fields {
3647 if let Some((pk_ident, pk_col)) = primary_key {
3648 let pk_column_lit = pk_col.as_str();
3649 // Two iterators — quote!'s `#(#var)*` consumes the
3650 // iterator, and we need to splice the same after-pairs
3651 // sequence into both per-backend arms.
3652 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3653 let pk_str = audit_pk_to_string.clone();
3654 // Per-tracked-column BEFORE-pair token list. Each entry
3655 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3656 // The Row alias resolves to PgRow / MySqlRow per call site,
3657 // so the same template generates both the PG and MySQL bodies.
3658 let mk_before_pairs =
3659 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3660 tracked
3661 .iter()
3662 .map(|c| {
3663 let column_lit = c.column.as_str();
3664 let value_ty = &c.value_ty;
3665 quote! {
3666 (
3667 #column_lit,
3668 match #getter::<#value_ty>(
3669 _audit_before_row, #column_lit,
3670 ) {
3671 ::core::result::Result::Ok(v) => {
3672 #root::__serde_json::to_value(&v)
3673 .unwrap_or(#root::__serde_json::Value::Null)
3674 }
3675 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3676 },
3677 )
3678 }
3679 })
3680 .collect()
3681 };
3682 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3683 mk_before_pairs(quote!(#root::sql::try_get_returning));
3684 let before_pairs_my: Vec<proc_macro2::TokenStream> =
3685 mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3686 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3687 mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3688 let pg_select_cols: String = tracked
3689 .iter()
3690 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3691 .collect::<Vec<_>>()
3692 .join(", ");
3693 let my_select_cols: String = tracked
3694 .iter()
3695 .map(|c| format!("`{}`", c.column.replace('`', "``")))
3696 .collect::<Vec<_>>()
3697 .join(", ");
3698 // SQLite uses double-quote identifier quoting (same as
3699 // Postgres in default config), so the column-list shape
3700 // matches PG.
3701 let sqlite_select_cols: String = pg_select_cols.clone();
3702 let pk_value_for_bind = if fields.pk_is_auto {
3703 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3704 } else {
3705 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3706 };
3707 let assignments = &fields.update_assignments;
3708 let unset_dispatch = if fields.has_auto {
3709 quote! {
3710 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3711 return self.insert_pool(pool).await;
3712 }
3713 }
3714 } else {
3715 quote!()
3716 };
3717 quote! {
3718 /// Save this row against either backend with audit
3719 /// emission (diff-style: BEFORE+AFTER) inside the
3720 /// same transaction. Auto::Unset PK routes to
3721 /// insert_pool. Bi-dialect counterpart of
3722 /// [`Self::save`] for audited models.
3723 ///
3724 /// The audit row's `changes` JSON contains one
3725 /// `{ "field": { "before": …, "after": … } }` entry
3726 /// per tracked column whose value actually changed
3727 /// — same shape as the existing &PgPool save() emits.
3728 ///
3729 /// # Errors
3730 /// As [`Self::save`].
3731 pub async fn save_pool(
3732 &mut self,
3733 pool: &#root::sql::Pool,
3734 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3735 #unset_dispatch
3736 let _query = #root::core::UpdateQuery {
3737 model: <Self as #root::core::Model>::SCHEMA,
3738 set: ::std::vec![ #( #assignments ),* ],
3739 where_clause: #root::core::WhereExpr::Predicate(
3740 #root::core::Filter {
3741 column: #pk_column_lit,
3742 op: #root::core::Op::Eq,
3743 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3744 ::core::clone::Clone::clone(&self.#pk_ident)
3745 ),
3746 }
3747 ),
3748 };
3749 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3750 ::std::vec![ #( #after_pairs_pg ),* ];
3751 #root::audit::save_one_with_diff(
3752 pool,
3753 &_query,
3754 #pk_column_lit,
3755 ::core::convert::Into::<#root::core::SqlValue>::into(
3756 #pk_value_for_bind,
3757 ),
3758 <Self as #root::core::Model>::SCHEMA.table,
3759 #pk_str,
3760 _after_pairs,
3761 #pg_select_cols,
3762 #my_select_cols,
3763 #sqlite_select_cols,
3764 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3765 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3766 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3767 ).await
3768 }
3769 }
3770 } else {
3771 quote!()
3772 }
3773 } else {
3774 pool_save_method
3775 };
3776
3777 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3778 // body shapes:
3779 // - non-audited: simple dispatch through `sql::delete_pool`
3780 // - audited: routes through `audit::delete_one_with_audit`,
3781 // which opens a per-backend transaction wrapping DELETE +
3782 // audit emit so the data write and audit row commit atomically.
3783 let pool_delete_method = {
3784 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3785 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3786 if let Some(pk_ident) = pk_ident_for_pool {
3787 if audited_fields.is_some() {
3788 let pairs = audit_pair_tokens.iter();
3789 let pk_str = audit_pk_to_string.clone();
3790 quote! {
3791 /// Delete this row against either backend with audit
3792 /// emission inside the same transaction. Bi-dialect
3793 /// counterpart of [`Self::delete`] for audited models.
3794 ///
3795 /// # Errors
3796 /// As [`Self::delete`].
3797 pub async fn delete_pool(
3798 &self,
3799 pool: &#root::sql::Pool,
3800 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3801 let _query = #root::core::DeleteQuery {
3802 model: <Self as #root::core::Model>::SCHEMA,
3803 where_clause: #root::core::WhereExpr::Predicate(
3804 #root::core::Filter {
3805 column: #pk_column_lit,
3806 op: #root::core::Op::Eq,
3807 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3808 ::core::clone::Clone::clone(&self.#pk_ident)
3809 ),
3810 }
3811 ),
3812 };
3813 let _audit_entry = #root::audit::PendingEntry {
3814 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3815 entity_pk: #pk_str,
3816 operation: #root::audit::AuditOp::Delete,
3817 source: #root::audit::current_source(),
3818 changes: #root::audit::snapshot_changes(&[
3819 #( #pairs ),*
3820 ]),
3821 };
3822 #root::audit::delete_one_with_audit(
3823 pool, &_query, &_audit_entry,
3824 ).await
3825 }
3826 }
3827 } else {
3828 quote! {
3829 /// Delete the row identified by this instance's primary key
3830 /// against either backend. Equivalent to [`Self::delete`] but
3831 /// takes [`#root::sql::Pool`] and dispatches per backend.
3832 ///
3833 /// # Errors
3834 /// As [`Self::delete`].
3835 pub async fn delete_pool(
3836 &self,
3837 pool: &#root::sql::Pool,
3838 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3839 let _query = #root::core::DeleteQuery {
3840 model: <Self as #root::core::Model>::SCHEMA,
3841 where_clause: #root::core::WhereExpr::Predicate(
3842 #root::core::Filter {
3843 column: #pk_column_lit,
3844 op: #root::core::Op::Eq,
3845 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3846 ::core::clone::Clone::clone(&self.#pk_ident)
3847 ),
3848 }
3849 ),
3850 };
3851 #root::sql::delete_pool(pool, &_query).await
3852 }
3853 }
3854 }
3855 } else {
3856 quote!()
3857 }
3858 };
3859
3860 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3861 // matching this instance's PK and overwrite the in-memory state
3862 // with the freshly-fetched columns. Django's `refresh_from_db`.
3863 // Issue #825. Only emitted when the model declares a PK; non-PK
3864 // models can't address a specific row.
3865 //
3866 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3867 // every field from `self`; resets the PK to `Auto::Unset` when
3868 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3869 // a fresh autoincrement value. Non-Auto PKs preserve the source
3870 // PK — the caller must overwrite before insert. Pure-Rust, no
3871 // I/O, no dialect surface.
3872 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3873 let other_field_clones: Vec<TokenStream2> = fields
3874 .column_entries
3875 .iter()
3876 .filter(|c| &c.ident != pk_ident)
3877 .map(|c| {
3878 let ident = &c.ident;
3879 quote! {
3880 #ident: ::core::clone::Clone::clone(&self.#ident)
3881 }
3882 })
3883 .collect();
3884 let pk_clone_token = if fields.pk_is_auto {
3885 quote! { #pk_ident: #root::sql::Auto::Unset }
3886 } else {
3887 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3888 };
3889 let replicate_doc = if fields.pk_is_auto {
3890 quote! {
3891 /// Eloquent-style `replicate()` — return a clone of this
3892 /// row with the primary key reset to [`Auto::Unset`] so
3893 /// the copy is ready for `insert_pool` / `save_pool` to
3894 /// allocate a fresh autoincrement value. Every other
3895 /// field is `Clone`d verbatim. Issue #825.
3896 ///
3897 /// `auto_now_add` / `auto_now` timestamp fields are
3898 /// **not** reset (Eloquent's `replicate` doesn't reset
3899 /// them either) — pass them through the normal insert
3900 /// path if you want fresh values, or assign them
3901 /// explicitly after the call.
3902 }
3903 } else {
3904 quote! {
3905 /// Eloquent-style `replicate()` — clone this row
3906 /// verbatim. Because the primary key is **not** an
3907 /// `Auto<T>`, the clone keeps the source PK; the
3908 /// caller must overwrite `copy.<pk>` before inserting
3909 /// to avoid a unique-key violation. Issue #825.
3910 }
3911 };
3912 // 2026-06-07 — field-name / shortcut collision guard.
3913 //
3914 // The macro emits both `pub const <field>: <field>_col = ...`
3915 // (per-field typed-column const, used by the typed-builder
3916 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3917 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3918 // model has a field named e.g. `count`, both items would land
3919 // in the same inherent impl with the same name, and the
3920 // compiler rejects the derive with "duplicate definitions".
3921 //
3922 // Drop the conflicting shortcut for that model. Callers can
3923 // still reach the same behavior via
3924 // `QuerySet::<Model>::default().count_pool(&pool)`.
3925 let column_names: ::std::collections::HashSet<String> = fields
3926 .column_entries
3927 .iter()
3928 .map(|c| c.ident.to_string())
3929 .collect();
3930 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3931 if column_names.contains(name) {
3932 quote! {}
3933 } else {
3934 tokens
3935 }
3936 };
3937 let count_method = emit_if_no_field_collision(
3938 "count",
3939 quote! {
3940 /// Count rows of this model — `SELECT COUNT(*) FROM
3941 /// <table>`. Eloquent `Model::count()` parity.
3942 ///
3943 /// Skipped on models that already declare a field named
3944 /// `count`. Drop into `QuerySet::<Self>::default().count_pool(&pool)`
3945 /// in that case.
3946 ///
3947 /// # Errors
3948 /// As [`CounterPool::count_pool`].
3949 ///
3950 /// [`CounterPool::count_pool`]: rustango::sql::CounterPool::count_pool
3951 pub async fn count(
3952 pool: &#root::sql::Pool,
3953 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3954 use #root::sql::CounterPool as _;
3955 #root::query::QuerySet::<Self>::default()
3956 .count_pool(pool)
3957 .await
3958 }
3959 },
3960 );
3961 let value_method = emit_if_no_field_collision(
3962 "value",
3963 quote! {
3964 /// Pluck a single scalar from the first row.
3965 /// Eloquent `Model::query()->value($col)` parity.
3966 ///
3967 /// Skipped on models that already declare a field named
3968 /// `value`. Drop into
3969 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
3970 ///
3971 /// # Errors
3972 /// As `ValuesFlatQuerySet::first`.
3973 pub async fn value<U>(
3974 col: &str,
3975 pool: &#root::sql::Pool,
3976 ) -> ::core::result::Result<
3977 ::core::option::Option<U>,
3978 #root::sql::ExecError,
3979 >
3980 where
3981 U: #root::sql::MaybePgScalar
3982 + #root::sql::MaybeMyScalar
3983 + #root::sql::MaybeSqliteScalar
3984 + ::core::marker::Send
3985 + ::core::marker::Unpin,
3986 {
3987 let _col_static: &'static str = Self::__resolve_col(col)?;
3988 #root::query::QuerySet::<Self>::default()
3989 .values_list_flat(_col_static)
3990 .first::<U>(pool)
3991 .await
3992 }
3993 },
3994 );
3995 let sum_method = emit_if_no_field_collision(
3996 "sum",
3997 quote! {
3998 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
3999 /// Skipped on models that already declare a field named
4000 /// `sum`.
4001 ///
4002 /// # Errors
4003 /// As [`#root::sql::fetch_aggregate_pool`].
4004 pub async fn sum<U>(
4005 col: &str,
4006 pool: &#root::sql::Pool,
4007 ) -> ::core::result::Result<
4008 ::core::option::Option<U>,
4009 #root::sql::ExecError,
4010 >
4011 where
4012 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4013 + #root::sql::MaybeMyFromRow
4014 + #root::sql::MaybeSqliteFromRow
4015 + ::core::marker::Send
4016 + ::core::marker::Unpin,
4017 {
4018 Self::__aggregate_one_pool::<U>(
4019 col,
4020 |c| #root::core::AggregateExpr::Sum(c),
4021 pool,
4022 )
4023 .await
4024 }
4025 },
4026 );
4027 let avg_method = emit_if_no_field_collision(
4028 "avg",
4029 quote! {
4030 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4031 /// models that already declare a field named `avg`.
4032 ///
4033 /// # Errors
4034 /// As [`#root::sql::fetch_aggregate_pool`].
4035 pub async fn avg<U>(
4036 col: &str,
4037 pool: &#root::sql::Pool,
4038 ) -> ::core::result::Result<
4039 ::core::option::Option<U>,
4040 #root::sql::ExecError,
4041 >
4042 where
4043 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4044 + #root::sql::MaybeMyFromRow
4045 + #root::sql::MaybeSqliteFromRow
4046 + ::core::marker::Send
4047 + ::core::marker::Unpin,
4048 {
4049 Self::__aggregate_one_pool::<U>(
4050 col,
4051 |c| #root::core::AggregateExpr::Avg(c),
4052 pool,
4053 )
4054 .await
4055 }
4056 },
4057 );
4058 let min_method = emit_if_no_field_collision(
4059 "min",
4060 quote! {
4061 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4062 /// models that already declare a field named `min`.
4063 ///
4064 /// # Errors
4065 /// As [`#root::sql::fetch_aggregate_pool`].
4066 pub async fn min<U>(
4067 col: &str,
4068 pool: &#root::sql::Pool,
4069 ) -> ::core::result::Result<
4070 ::core::option::Option<U>,
4071 #root::sql::ExecError,
4072 >
4073 where
4074 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4075 + #root::sql::MaybeMyFromRow
4076 + #root::sql::MaybeSqliteFromRow
4077 + ::core::marker::Send
4078 + ::core::marker::Unpin,
4079 {
4080 Self::__aggregate_one_pool::<U>(
4081 col,
4082 |c| #root::core::AggregateExpr::Min(c),
4083 pool,
4084 )
4085 .await
4086 }
4087 },
4088 );
4089 let max_method = emit_if_no_field_collision(
4090 "max",
4091 quote! {
4092 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4093 /// models that already declare a field named `max`.
4094 ///
4095 /// # Errors
4096 /// As [`#root::sql::fetch_aggregate_pool`].
4097 pub async fn max<U>(
4098 col: &str,
4099 pool: &#root::sql::Pool,
4100 ) -> ::core::result::Result<
4101 ::core::option::Option<U>,
4102 #root::sql::ExecError,
4103 >
4104 where
4105 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4106 + #root::sql::MaybeMyFromRow
4107 + #root::sql::MaybeSqliteFromRow
4108 + ::core::marker::Send
4109 + ::core::marker::Unpin,
4110 {
4111 Self::__aggregate_one_pool::<U>(
4112 col,
4113 |c| #root::core::AggregateExpr::Max(c),
4114 pool,
4115 )
4116 .await
4117 }
4118 },
4119 );
4120 let first_method = emit_if_no_field_collision(
4121 "first",
4122 quote! {
4123 /// First row of this model. Eloquent `Model::first()`.
4124 /// Skipped on models that already declare a field named
4125 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4126 ///
4127 /// # Errors
4128 /// As `QuerySet::first`.
4129 pub async fn first(
4130 pool: &#root::sql::Pool,
4131 ) -> ::core::result::Result<
4132 ::core::option::Option<Self>,
4133 #root::sql::ExecError,
4134 > {
4135 #root::query::QuerySet::<Self>::default()
4136 .first(pool)
4137 .await
4138 }
4139 },
4140 );
4141 let last_method = emit_if_no_field_collision(
4142 "last",
4143 quote! {
4144 /// Last row of this model by primary-key DESC.
4145 /// Eloquent `Model::query()->latest('id')->first()`
4146 /// parity — fetches the highest-PK row without
4147 /// requiring the caller to spell the PK column.
4148 /// Returns `None` on an empty table.
4149 ///
4150 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4151 /// Skipped on models that already declare a field
4152 /// named `last`.
4153 ///
4154 /// # Errors
4155 /// As `QuerySet::last`.
4156 pub async fn last(
4157 pool: &#root::sql::Pool,
4158 ) -> ::core::result::Result<
4159 ::core::option::Option<Self>,
4160 #root::sql::ExecError,
4161 > {
4162 #root::query::QuerySet::<Self>::default()
4163 .last(pool)
4164 .await
4165 }
4166 },
4167 );
4168 quote! {
4169 /// Re-SELECT this row by its primary key and overwrite
4170 /// every in-memory field with the freshly-fetched value.
4171 /// Django's [`Model.refresh_from_db`]. Issue #825.
4172 ///
4173 /// Use this when the row may have been modified by another
4174 /// process / connection / job since you read it — e.g. after
4175 /// a queued task callback, or to re-sync stale UI state
4176 /// before re-saving.
4177 ///
4178 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4179 /// when the primary key no longer matches any row (e.g.
4180 /// the row was deleted concurrently).
4181 ///
4182 /// # Errors
4183 /// As [`FetcherPool::fetch_pool`]; also `RowNotFound` when
4184 /// the PK no longer exists.
4185 ///
4186 /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4187 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4188 pub async fn refresh_from_db(
4189 &mut self,
4190 pool: &#root::sql::Pool,
4191 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4192 use #root::sql::FetcherPool as _;
4193 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4194 ::core::clone::Clone::clone(&self.#pk_ident),
4195 );
4196 let mut _rows: ::std::vec::Vec<Self> =
4197 #root::query::QuerySet::<Self>::default()
4198 .filter(::core::stringify!(#pk_ident), _pk_val)
4199 .limit(1)
4200 .fetch_pool(pool)
4201 .await?;
4202 match _rows.into_iter().next() {
4203 ::core::option::Option::Some(_fresh) => {
4204 *self = _fresh;
4205 ::core::result::Result::Ok(())
4206 }
4207 ::core::option::Option::None => ::core::result::Result::Err(
4208 #root::sql::ExecError::Driver(
4209 #root::sql::sqlx::Error::RowNotFound,
4210 ),
4211 ),
4212 }
4213 }
4214
4215 /// Atomically increment the integer column `col` by
4216 /// `by` for this row. Equivalent to
4217 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4218 /// Eloquent `Model::increment($col, $by)` / Django
4219 /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4220 /// parity.
4221 ///
4222 /// **Doesn't mutate `self`** — the in-memory copy is now
4223 /// stale; call [`Self::refresh_from_db_pool`] /
4224 /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4225 /// affected count (0 when the PK doesn't match any row,
4226 /// 1 on success).
4227 ///
4228 /// `col` is the Rust field name as a string; unknown
4229 /// fields surface as `UnknownField` at runtime. Negative
4230 /// `by` values atomically decrement (see also
4231 /// [`Self::decrement_pool`]).
4232 ///
4233 /// # Errors
4234 /// As [`UpdaterPool::execute_pool`].
4235 ///
4236 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4237 pub async fn increment(
4238 &self,
4239 col: &str,
4240 by: i64,
4241 pool: &#root::sql::Pool,
4242 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4243 Self::__increment_one(self, col, by, pool).await
4244 }
4245
4246 /// Sibling of [`Self::increment`] — atomically
4247 /// decrement this row's `col` by `by`. Eloquent
4248 /// `$model->decrement($col, $by)` parity. Equivalent to
4249 /// `self.increment(col, -by, &pool)`; the separate name
4250 /// keeps call sites readable.
4251 ///
4252 /// # Errors
4253 /// As [`Self::increment`].
4254 pub async fn decrement(
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 /// Bulk-increment: add `by` to `col` on every row of the
4264 /// table. Eloquent `Model::query()->increment($col, $by)`
4265 /// parity. Use for counters, score adjustments, view
4266 /// rollups.
4267 ///
4268 /// # Errors
4269 /// As [`UpdaterPool::execute_pool`].
4270 ///
4271 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4272 pub async fn increment_each(
4273 col: &str,
4274 by: i64,
4275 pool: &#root::sql::Pool,
4276 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4277 Self::__increment_all(col, by, pool).await
4278 }
4279
4280 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4281 ///
4282 /// # Errors
4283 /// As [`Self::increment_each`].
4284 pub async fn decrement_each(
4285 col: &str,
4286 by: i64,
4287 pool: &#root::sql::Pool,
4288 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4289 Self::__increment_all(col, -by, pool).await
4290 }
4291
4292 /// Internal: forward to
4293 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4294 /// One-line wrapper kept as a per-Model method so the
4295 /// macro's emitted `increment` / `decrement` instance
4296 /// calls don't have to thread `Self` through manually.
4297 #[doc(hidden)]
4298 pub async fn __increment_one(
4299 this: &Self,
4300 col: &str,
4301 by: i64,
4302 pool: &#root::sql::Pool,
4303 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4304 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4305 ::core::clone::Clone::clone(&this.#pk_ident),
4306 );
4307 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4308 ::core::stringify!(#pk_ident),
4309 _pk_val,
4310 col,
4311 by,
4312 pool,
4313 )
4314 .await
4315 }
4316
4317 /// Internal: forward to
4318 /// [`#root::sql::model_shortcuts::increment_all_pool`].
4319 #[doc(hidden)]
4320 pub async fn __increment_all(
4321 col: &str,
4322 by: i64,
4323 pool: &#root::sql::Pool,
4324 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4325 #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4326 }
4327
4328 /// Internal: forward to
4329 /// [`#root::sql::model_shortcuts::resolve_col`].
4330 #[doc(hidden)]
4331 pub fn __resolve_col(
4332 col: &str,
4333 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4334 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4335 }
4336
4337 /// Internal: forward to
4338 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4339 #[doc(hidden)]
4340 #[must_use]
4341 pub fn __add_signed_expr(
4342 col_static: &'static str,
4343 signed_by: i64,
4344 ) -> #root::core::Expr {
4345 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4346 }
4347
4348 /// Re-SELECT this row by its primary key and return a
4349 /// **new** instance with the freshly-fetched fields.
4350 /// Eloquent `Model::fresh()` parity — non-mutating
4351 /// counterpart of [`Self::refresh_from_db_pool`].
4352 ///
4353 /// Returns `Ok(None)` when the row was deleted
4354 /// concurrently — vs [`Self::refresh_from_db_pool`]
4355 /// which surfaces that as `RowNotFound` because
4356 /// in-place mutation has nothing to write to.
4357 ///
4358 /// Useful when you want to compare the in-memory
4359 /// instance against the persisted state (audit-style
4360 /// diffs, conflict detection) without mutating the
4361 /// reference you already hold.
4362 ///
4363 /// # Errors
4364 /// As [`FetcherPool::fetch_pool`].
4365 ///
4366 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4367 pub async fn fresh(
4368 &self,
4369 pool: &#root::sql::Pool,
4370 ) -> ::core::result::Result<
4371 ::core::option::Option<Self>,
4372 #root::sql::ExecError,
4373 > {
4374 use #root::sql::FetcherPool as _;
4375 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4376 ::core::clone::Clone::clone(&self.#pk_ident),
4377 );
4378 let _rows: ::std::vec::Vec<Self> =
4379 #root::query::QuerySet::<Self>::default()
4380 .filter(::core::stringify!(#pk_ident), _pk_val)
4381 .limit(1)
4382 .fetch_pool(pool)
4383 .await?;
4384 ::core::result::Result::Ok(_rows.into_iter().next())
4385 }
4386
4387 #replicate_doc
4388 #[must_use]
4389 pub fn replicate(&self) -> Self {
4390 Self {
4391 #pk_clone_token,
4392 #( #other_field_clones, )*
4393 }
4394 }
4395
4396 #first_method
4397
4398 #last_method
4399
4400 /// Throwing counterpart of [`Self::first_pool`] —
4401 /// errors with `RowNotFound` when the table is empty.
4402 /// Eloquent `Model::firstOrFail()` parity.
4403 ///
4404 /// # Errors
4405 /// As [`Self::first_pool`]; additionally
4406 /// [`sqlx::Error::RowNotFound`] on empty tables.
4407 ///
4408 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4409 pub async fn first_or_fail(
4410 pool: &#root::sql::Pool,
4411 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4412 // Route through the queryset rather than `Self::first`,
4413 // which is suppressed on models with a field named
4414 // `first` (field/shortcut collision guard).
4415 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4416 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4417 ::core::option::Option::None => ::core::result::Result::Err(
4418 #root::sql::ExecError::Driver(
4419 #root::sql::sqlx::Error::RowNotFound,
4420 ),
4421 ),
4422 }
4423 }
4424
4425 /// Single-column projection — `SELECT <col> FROM
4426 /// <table>`. Returns `Vec<U>` where each element is the
4427 /// decoded value of the column. Eloquent
4428 /// `Model::pluck($column)` / Django
4429 /// `Model.objects.values_list('col', flat=True)` parity.
4430 ///
4431 /// Thin wrapper over `QuerySet::<Self>::default()
4432 /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4433 /// be decodable from the column's SQL type on every
4434 /// dialect the binary targets (common picks: `i64` /
4435 /// `i32` / `String` / `bool` / `f64`).
4436 ///
4437 /// # Errors
4438 /// As `ValuesFlatQuerySet::fetch`.
4439 pub async fn pluck<U>(
4440 col: &'static str,
4441 pool: &#root::sql::Pool,
4442 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4443 where
4444 U: #root::sql::MaybePgScalar
4445 + #root::sql::MaybeMyScalar
4446 + #root::sql::MaybeSqliteScalar
4447 + ::core::marker::Send
4448 + ::core::marker::Unpin,
4449 {
4450 #root::query::QuerySet::<Self>::default()
4451 .values_list_flat(col)
4452 .fetch::<U>(pool)
4453 .await
4454 }
4455
4456 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4457 /// stream every row of this model in batches of `n`,
4458 /// invoking the callback once per batch. Stable PK-ASC
4459 /// ordering so the LIMIT/OFFSET pagination is
4460 /// deterministic across drivers.
4461 ///
4462 /// The callback is async — it can do further DB work
4463 /// (writes, related-row lookups, queue dispatch) per
4464 /// batch. Return `Err(...)` from the callback to abort
4465 /// the iteration early; the error bubbles up.
4466 ///
4467 /// **When to use**: bulk processing flows that can't
4468 /// fit the whole table in memory — sending newsletters,
4469 /// running data migrations, computing summary
4470 /// statistics. For small / known-bounded tables, plain
4471 /// `Self::all(&pool)` is simpler.
4472 ///
4473 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4474 /// large tables. For multi-million-row scans prefer
4475 /// keyset-by-PK (the standard "WHERE id > last_seen"
4476 /// shape) over `chunk(...)`.
4477 ///
4478 /// Skipped on models without a primary key — chunking
4479 /// needs a stable order to avoid skipping / repeating
4480 /// rows across batches.
4481 pub async fn chunk<F, Fut>(
4482 n: i64,
4483 pool: &#root::sql::Pool,
4484 mut cb: F,
4485 ) -> ::core::result::Result<(), #root::sql::ExecError>
4486 where
4487 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4488 Fut: ::core::future::Future<
4489 Output = ::core::result::Result<(), #root::sql::ExecError>,
4490 >,
4491 {
4492 use #root::sql::FetcherPool as _;
4493 let pk_col = match Self::primary_key_column() {
4494 ::core::option::Option::Some(c) => c,
4495 ::core::option::Option::None => {
4496 return ::core::result::Result::Ok(());
4497 }
4498 };
4499 let mut offset: i64 = 0;
4500 loop {
4501 let rows: ::std::vec::Vec<Self> =
4502 #root::query::QuerySet::<Self>::default()
4503 .order_by(&[(pk_col, false)])
4504 .limit(n)
4505 .offset(offset)
4506 .fetch_pool(pool)
4507 .await?;
4508 if rows.is_empty() {
4509 return ::core::result::Result::Ok(());
4510 }
4511 let len = rows.len() as i64;
4512 cb(rows).await?;
4513 if len < n {
4514 return ::core::result::Result::Ok(());
4515 }
4516 offset += n;
4517 }
4518 }
4519
4520 /// Eloquent `Model::chunkById($n, fn (...))` — same
4521 /// per-batch callback shape as [`Self::chunk`], but uses
4522 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4523 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4524 /// — the right choice for multi-million-row sweeps.
4525 ///
4526 /// Requires the primary key to be a signed integer type
4527 /// (`i64` / `i32`); the keyset comparison rides on
4528 /// `__rustango_pk_value()` lowering through
4529 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4530 /// non-integer PKs (UUID, String) — those should use
4531 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4532 /// keyset loop.
4533 ///
4534 /// Callback errors abort iteration; the error bubbles up
4535 /// unchanged. Empty table → callback invoked zero times.
4536 pub async fn chunk_by_id<F, Fut>(
4537 n: i64,
4538 pool: &#root::sql::Pool,
4539 mut cb: F,
4540 ) -> ::core::result::Result<(), #root::sql::ExecError>
4541 where
4542 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4543 Fut: ::core::future::Future<
4544 Output = ::core::result::Result<(), #root::sql::ExecError>,
4545 >,
4546 {
4547 use #root::sql::FetcherPool as _;
4548 let pk_col = match Self::primary_key_column() {
4549 ::core::option::Option::Some(c) => c,
4550 ::core::option::Option::None => {
4551 return ::core::result::Result::Ok(());
4552 }
4553 };
4554 // Track the largest PK seen so the next batch picks
4555 // up from there. `i64::MIN` as the sentinel — the
4556 // very first iteration's `> MIN` matches every row,
4557 // so the loop entry is uniform with subsequent
4558 // iterations.
4559 let mut last_seen: i64 = i64::MIN;
4560 loop {
4561 let key = ::std::format!("{}__gt", pk_col);
4562 let rows: ::std::vec::Vec<Self> =
4563 #root::query::QuerySet::<Self>::default()
4564 .filter(key.as_str(), last_seen)
4565 .order_by(&[(pk_col, false)])
4566 .limit(n)
4567 .fetch_pool(pool)
4568 .await?;
4569 if rows.is_empty() {
4570 return ::core::result::Result::Ok(());
4571 }
4572 let len = rows.len() as i64;
4573 // Capture the last row's PK BEFORE moving rows
4574 // into the callback.
4575 let max_pk = match rows
4576 .last()
4577 .map(|r| r.__rustango_pk_value())
4578 {
4579 ::core::option::Option::Some(
4580 #root::core::SqlValue::I64(v),
4581 ) => v,
4582 ::core::option::Option::Some(
4583 #root::core::SqlValue::I32(v),
4584 ) => i64::from(v),
4585 _ => return ::core::result::Result::Ok(()),
4586 };
4587 cb(rows).await?;
4588 if len < n {
4589 return ::core::result::Result::Ok(());
4590 }
4591 last_seen = max_pk;
4592 }
4593 }
4594
4595 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4596 /// per-row callback companion to [`Self::chunk`].
4597 /// Streams every row in keyset-paginated batches of
4598 /// `batch` size, calling `cb` once per row.
4599 ///
4600 /// Inherits the keyset-paginated scan of
4601 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4602 /// non-integer PKs are silently a no-op).
4603 ///
4604 /// ```ignore
4605 /// Post::each(500, &pool, |p| async move {
4606 /// reindex(p).await?;
4607 /// Ok(())
4608 /// }).await?;
4609 /// ```
4610 ///
4611 /// Return `Err(...)` from the callback to abort
4612 /// iteration; the error bubbles up unchanged.
4613 pub async fn each<F, Fut>(
4614 batch: i64,
4615 pool: &#root::sql::Pool,
4616 mut cb: F,
4617 ) -> ::core::result::Result<(), #root::sql::ExecError>
4618 where
4619 F: ::core::ops::FnMut(Self) -> Fut,
4620 Fut: ::core::future::Future<
4621 Output = ::core::result::Result<(), #root::sql::ExecError>,
4622 >,
4623 {
4624 use #root::sql::FetcherPool as _;
4625 let pk_col = match Self::primary_key_column() {
4626 ::core::option::Option::Some(c) => c,
4627 ::core::option::Option::None => {
4628 return ::core::result::Result::Ok(());
4629 }
4630 };
4631 let mut last_seen: i64 = i64::MIN;
4632 loop {
4633 let key = ::std::format!("{}__gt", pk_col);
4634 let rows: ::std::vec::Vec<Self> =
4635 #root::query::QuerySet::<Self>::default()
4636 .filter(key.as_str(), last_seen)
4637 .order_by(&[(pk_col, false)])
4638 .limit(batch)
4639 .fetch_pool(pool)
4640 .await?;
4641 if rows.is_empty() {
4642 return ::core::result::Result::Ok(());
4643 }
4644 let len = rows.len() as i64;
4645 let max_pk = match rows
4646 .last()
4647 .map(|r| r.__rustango_pk_value())
4648 {
4649 ::core::option::Option::Some(
4650 #root::core::SqlValue::I64(v),
4651 ) => v,
4652 ::core::option::Option::Some(
4653 #root::core::SqlValue::I32(v),
4654 ) => i64::from(v),
4655 _ => return ::core::result::Result::Ok(()),
4656 };
4657 for row in rows {
4658 cb(row).await?;
4659 }
4660 if len < batch {
4661 return ::core::result::Result::Ok(());
4662 }
4663 last_seen = max_pk;
4664 }
4665 }
4666
4667 /// Delete every row of this model — `TRUNCATE TABLE
4668 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4669 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4670 /// support `TRUNCATE` inside foreign-key constraints
4671 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4672 /// / Django `Model.objects.all().delete()` parity.
4673 ///
4674 /// **Use only in tests / fixture-reset flows.** Production
4675 /// writes through this would silently bypass the
4676 /// `pre_delete` / `post_delete` signals (no per-row hooks
4677 /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4678 /// row's audit-log entry.
4679 ///
4680 /// # Errors
4681 /// As [`raw_execute_pool`].
4682 ///
4683 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4684 pub async fn truncate(
4685 pool: &#root::sql::Pool,
4686 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4687 let _table = <Self as #root::core::Model>::SCHEMA.table;
4688 let _dialect = pool.dialect();
4689 let _quoted = _dialect.quote_ident(_table);
4690 let _sql = if _dialect.name() == "postgres" {
4691 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4692 } else {
4693 ::std::format!("DELETE FROM {}", _quoted)
4694 };
4695 #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4696 }
4697
4698 /// Bulk-delete every row whose primary key is in
4699 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4700 /// Returns the affected row count.
4701 ///
4702 /// Eloquent `Model::destroy([1, 2, 3])` / Django
4703 /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4704 /// Empty `pks` is a no-op (returns 0).
4705 ///
4706 /// Accepts any iterable whose elements are
4707 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4708 /// `[i64; N]`, etc.
4709 ///
4710 /// # Errors
4711 /// As `delete_pool`.
4712 pub async fn destroy<V>(
4713 pks: impl ::core::iter::IntoIterator<Item = V>,
4714 pool: &#root::sql::Pool,
4715 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4716 where
4717 V: ::core::convert::Into<#root::core::SqlValue>,
4718 {
4719 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4720 pks.into_iter().map(::core::convert::Into::into).collect();
4721 if _values.is_empty() {
4722 return ::core::result::Result::Ok(0);
4723 }
4724 let _query = #root::core::DeleteQuery {
4725 model: <Self as #root::core::Model>::SCHEMA,
4726 where_clause: #root::core::WhereExpr::Predicate(
4727 #root::core::Filter {
4728 column: <Self as #root::core::Model>::SCHEMA
4729 .primary_key()
4730 .ok_or_else(|| {
4731 #root::sql::ExecError::Sql(
4732 #root::sql::SqlError::MissingPrimaryKey,
4733 )
4734 })?
4735 .column,
4736 op: #root::core::Op::In,
4737 value: #root::core::SqlValue::List(_values),
4738 },
4739 ),
4740 };
4741 #root::sql::delete_pool(pool, &_query).await
4742 }
4743
4744 /// Fetch every row where `<col> = <val>`. Eloquent
4745 /// `Model::where($col, $val)->get()` / Django
4746 /// `Model.objects.filter(col=val).all()` parity.
4747 ///
4748 /// Thin wrapper over `QuerySet::<Self>::default()
4749 /// .filter(col, val).fetch_pool(pool)`. For one row,
4750 /// use [`Self::first_where_pool`]; for a chain that
4751 /// needs further `.filter()` / `.order_by()` /
4752 /// `.limit()`, drop down to `Self::query().filter(...)`
4753 /// directly.
4754 ///
4755 /// `val` accepts any value `Into<SqlValue>` so plain
4756 /// strings, ints, UUIDs, etc. all work.
4757 ///
4758 /// # Errors
4759 /// As [`FetcherPool::fetch_pool`].
4760 ///
4761 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4762 pub async fn where_(
4763 col: &str,
4764 val: impl ::core::convert::Into<#root::core::SqlValue>,
4765 pool: &#root::sql::Pool,
4766 ) -> ::core::result::Result<
4767 ::std::vec::Vec<Self>,
4768 #root::sql::ExecError,
4769 > {
4770 use #root::sql::FetcherPool as _;
4771 #root::query::QuerySet::<Self>::default()
4772 .filter(col, val)
4773 .fetch_pool(pool)
4774 .await
4775 }
4776
4777 /// Fetch every row where `<col> IN (vals)`. Eloquent
4778 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4779 /// `vals` returns no rows (matches SQL's empty-IN
4780 /// semantics).
4781 ///
4782 /// `vals` accepts any iterable whose items are
4783 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4784 /// etc.
4785 ///
4786 /// # Errors
4787 /// As [`FetcherPool::fetch_pool`].
4788 ///
4789 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4790 pub async fn where_in<V>(
4791 col: &str,
4792 vals: impl ::core::iter::IntoIterator<Item = V>,
4793 pool: &#root::sql::Pool,
4794 ) -> ::core::result::Result<
4795 ::std::vec::Vec<Self>,
4796 #root::sql::ExecError,
4797 >
4798 where
4799 V: ::core::convert::Into<#root::core::SqlValue>,
4800 {
4801 use #root::sql::FetcherPool as _;
4802 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4803 vals.into_iter().map(::core::convert::Into::into).collect();
4804 if _values.is_empty() {
4805 return ::core::result::Result::Ok(::std::vec::Vec::new());
4806 }
4807 let _key = ::std::format!("{}__in", col);
4808 #root::query::QuerySet::<Self>::default()
4809 .filter(&_key, #root::core::SqlValue::List(_values))
4810 .fetch_pool(pool)
4811 .await
4812 }
4813
4814 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4815 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4816 /// `vals` returns every row (matches SQL's empty-NOT-IN
4817 /// semantics — vacuously true for every row).
4818 ///
4819 /// # Errors
4820 /// As [`FetcherPool::fetch_pool`].
4821 ///
4822 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4823 pub async fn where_not_in<V>(
4824 col: &str,
4825 vals: impl ::core::iter::IntoIterator<Item = V>,
4826 pool: &#root::sql::Pool,
4827 ) -> ::core::result::Result<
4828 ::std::vec::Vec<Self>,
4829 #root::sql::ExecError,
4830 >
4831 where
4832 V: ::core::convert::Into<#root::core::SqlValue>,
4833 {
4834 use #root::sql::FetcherPool as _;
4835 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4836 vals.into_iter().map(::core::convert::Into::into).collect();
4837 if _values.is_empty() {
4838 return #root::query::QuerySet::<Self>::default()
4839 .fetch_pool(pool)
4840 .await;
4841 }
4842 let _key = ::std::format!("{}__not_in", col);
4843 #root::query::QuerySet::<Self>::default()
4844 .filter(&_key, #root::core::SqlValue::List(_values))
4845 .fetch_pool(pool)
4846 .await
4847 }
4848
4849 /// Fetch every row where `<col> IS NULL`. Eloquent
4850 /// `Model::whereNull($col)->get()` parity.
4851 ///
4852 /// # Errors
4853 /// As [`FetcherPool::fetch_pool`].
4854 ///
4855 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4856 pub async fn where_null(
4857 col: &str,
4858 pool: &#root::sql::Pool,
4859 ) -> ::core::result::Result<
4860 ::std::vec::Vec<Self>,
4861 #root::sql::ExecError,
4862 > {
4863 use #root::sql::FetcherPool as _;
4864 let _key = ::std::format!("{}__isnull", col);
4865 #root::query::QuerySet::<Self>::default()
4866 .filter(&_key, #root::core::SqlValue::Bool(true))
4867 .fetch_pool(pool)
4868 .await
4869 }
4870
4871 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4872 /// `Model::whereNotNull($col)->get()` parity.
4873 ///
4874 /// # Errors
4875 /// As [`FetcherPool::fetch_pool`].
4876 ///
4877 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4878 pub async fn where_not_null(
4879 col: &str,
4880 pool: &#root::sql::Pool,
4881 ) -> ::core::result::Result<
4882 ::std::vec::Vec<Self>,
4883 #root::sql::ExecError,
4884 > {
4885 use #root::sql::FetcherPool as _;
4886 let _key = ::std::format!("{}__isnull", col);
4887 #root::query::QuerySet::<Self>::default()
4888 .filter(&_key, #root::core::SqlValue::Bool(false))
4889 .fetch_pool(pool)
4890 .await
4891 }
4892
4893 /// Fetch up to `n` rows in random order. Eloquent
4894 /// `Model::inRandomOrder()->limit($n)->get()` /
4895 /// `Model::query()->inRandomOrder()->get()->take($n)`
4896 /// parity. **Performance caveat**: random ordering
4897 /// forces a full table scan + per-row random key sort;
4898 /// the optimizer cannot use an index. Prefer a
4899 /// `pk >= rand_offset LIMIT N` walk for huge tables.
4900 ///
4901 /// # Errors
4902 /// As [`FetcherPool::fetch_pool`].
4903 ///
4904 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4905 pub async fn random_n(
4906 n: i64,
4907 pool: &#root::sql::Pool,
4908 ) -> ::core::result::Result<
4909 ::std::vec::Vec<Self>,
4910 #root::sql::ExecError,
4911 > {
4912 use #root::sql::FetcherPool as _;
4913 #root::query::QuerySet::<Self>::default()
4914 .order_random()
4915 .limit(n)
4916 .fetch_pool(pool)
4917 .await
4918 }
4919
4920 /// Fetch one row in random order. Eloquent
4921 /// `Model::inRandomOrder()->first()` parity. Same
4922 /// performance caveat as [`Self::random_n_pool`].
4923 ///
4924 /// # Errors
4925 /// As [`FetcherPool::fetch_pool`].
4926 ///
4927 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4928 pub async fn random(
4929 pool: &#root::sql::Pool,
4930 ) -> ::core::result::Result<
4931 ::core::option::Option<Self>,
4932 #root::sql::ExecError,
4933 > {
4934 ::core::result::Result::Ok(
4935 Self::random_n(1, pool).await?.into_iter().next(),
4936 )
4937 }
4938
4939 /// Fetch every row ordered ASC by `field`. Eloquent
4940 /// `Model::oldest($field)->get()` parity — the multi-row
4941 /// counterpart of [`Self::earliest_pool`].
4942 ///
4943 /// # Errors
4944 /// As [`FetcherPool::fetch_pool`].
4945 ///
4946 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4947 pub async fn oldest(
4948 field: &str,
4949 pool: &#root::sql::Pool,
4950 ) -> ::core::result::Result<
4951 ::std::vec::Vec<Self>,
4952 #root::sql::ExecError,
4953 > {
4954 use #root::sql::FetcherPool as _;
4955 #root::query::QuerySet::<Self>::default()
4956 .order_by(&[(field, false)])
4957 .fetch_pool(pool)
4958 .await
4959 }
4960
4961 /// Fetch every row ordered DESC by `field`. Eloquent
4962 /// `Model::latest($field)->get()` parity — the multi-row
4963 /// counterpart of [`Self::latest_pool`].
4964 ///
4965 /// # Errors
4966 /// As [`FetcherPool::fetch_pool`].
4967 ///
4968 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4969 pub async fn newest(
4970 field: &str,
4971 pool: &#root::sql::Pool,
4972 ) -> ::core::result::Result<
4973 ::std::vec::Vec<Self>,
4974 #root::sql::ExecError,
4975 > {
4976 use #root::sql::FetcherPool as _;
4977 #root::query::QuerySet::<Self>::default()
4978 .order_by(&[(field, true)])
4979 .fetch_pool(pool)
4980 .await
4981 }
4982
4983 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
4984 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
4985 /// Routes through the existing `__year` lookup suffix
4986 /// (issue #829).
4987 ///
4988 /// # Errors
4989 /// As [`FetcherPool::fetch_pool`].
4990 ///
4991 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4992 pub async fn where_year(
4993 col: &str,
4994 year: i64,
4995 pool: &#root::sql::Pool,
4996 ) -> ::core::result::Result<
4997 ::std::vec::Vec<Self>,
4998 #root::sql::ExecError,
4999 > {
5000 use #root::sql::FetcherPool as _;
5001 let _key = ::std::format!("{}__year", col);
5002 #root::query::QuerySet::<Self>::default()
5003 .filter(&_key, #root::core::SqlValue::I64(year))
5004 .fetch_pool(pool)
5005 .await
5006 }
5007
5008 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5009 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5010 /// `month` is 1–12.
5011 ///
5012 /// # Errors
5013 /// As [`FetcherPool::fetch_pool`].
5014 ///
5015 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5016 pub async fn where_month(
5017 col: &str,
5018 month: i64,
5019 pool: &#root::sql::Pool,
5020 ) -> ::core::result::Result<
5021 ::std::vec::Vec<Self>,
5022 #root::sql::ExecError,
5023 > {
5024 use #root::sql::FetcherPool as _;
5025 let _key = ::std::format!("{}__month", col);
5026 #root::query::QuerySet::<Self>::default()
5027 .filter(&_key, #root::core::SqlValue::I64(month))
5028 .fetch_pool(pool)
5029 .await
5030 }
5031
5032 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5033 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5034 /// `day` is 1–31.
5035 ///
5036 /// # Errors
5037 /// As [`FetcherPool::fetch_pool`].
5038 ///
5039 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5040 pub async fn where_day(
5041 col: &str,
5042 day: i64,
5043 pool: &#root::sql::Pool,
5044 ) -> ::core::result::Result<
5045 ::std::vec::Vec<Self>,
5046 #root::sql::ExecError,
5047 > {
5048 use #root::sql::FetcherPool as _;
5049 let _key = ::std::format!("{}__day", col);
5050 #root::query::QuerySet::<Self>::default()
5051 .filter(&_key, #root::core::SqlValue::I64(day))
5052 .fetch_pool(pool)
5053 .await
5054 }
5055
5056 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5057 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5058 /// `hour` is 0–23.
5059 ///
5060 /// # Errors
5061 /// As [`FetcherPool::fetch_pool`].
5062 ///
5063 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5064 pub async fn where_hour(
5065 col: &str,
5066 hour: i64,
5067 pool: &#root::sql::Pool,
5068 ) -> ::core::result::Result<
5069 ::std::vec::Vec<Self>,
5070 #root::sql::ExecError,
5071 > {
5072 use #root::sql::FetcherPool as _;
5073 let _key = ::std::format!("{}__hour", col);
5074 #root::query::QuerySet::<Self>::default()
5075 .filter(&_key, #root::core::SqlValue::I64(hour))
5076 .fetch_pool(pool)
5077 .await
5078 }
5079
5080 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5081 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5082 /// `minute` is 0–59.
5083 ///
5084 /// # Errors
5085 /// As [`FetcherPool::fetch_pool`].
5086 ///
5087 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5088 pub async fn where_minute(
5089 col: &str,
5090 minute: i64,
5091 pool: &#root::sql::Pool,
5092 ) -> ::core::result::Result<
5093 ::std::vec::Vec<Self>,
5094 #root::sql::ExecError,
5095 > {
5096 use #root::sql::FetcherPool as _;
5097 let _key = ::std::format!("{}__minute", col);
5098 #root::query::QuerySet::<Self>::default()
5099 .filter(&_key, #root::core::SqlValue::I64(minute))
5100 .fetch_pool(pool)
5101 .await
5102 }
5103
5104 /// Fetch every row where `<col> LIKE <pattern>` —
5105 /// caller-supplied pattern (must include `%` / `_`
5106 /// wildcards manually). Eloquent `Model::whereLike`
5107 /// parity.
5108 ///
5109 /// # Errors
5110 /// As [`FetcherPool::fetch_pool`].
5111 ///
5112 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5113 pub async fn where_like(
5114 col: &str,
5115 pattern: impl ::core::convert::Into<::std::string::String>,
5116 pool: &#root::sql::Pool,
5117 ) -> ::core::result::Result<
5118 ::std::vec::Vec<Self>,
5119 #root::sql::ExecError,
5120 > {
5121 use #root::sql::FetcherPool as _;
5122 let _key = ::std::format!("{}__like", col);
5123 #root::query::QuerySet::<Self>::default()
5124 .filter(
5125 &_key,
5126 #root::core::SqlValue::String(pattern.into()),
5127 )
5128 .fetch_pool(pool)
5129 .await
5130 }
5131
5132 /// Fetch every row where `<col> ILIKE <pattern>` —
5133 /// case-insensitive LIKE (PG native, MySQL/SQLite
5134 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5135 ///
5136 /// # Errors
5137 /// As [`FetcherPool::fetch_pool`].
5138 ///
5139 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5140 pub async fn where_ilike(
5141 col: &str,
5142 pattern: impl ::core::convert::Into<::std::string::String>,
5143 pool: &#root::sql::Pool,
5144 ) -> ::core::result::Result<
5145 ::std::vec::Vec<Self>,
5146 #root::sql::ExecError,
5147 > {
5148 use #root::sql::FetcherPool as _;
5149 let _key = ::std::format!("{}__ilike", col);
5150 #root::query::QuerySet::<Self>::default()
5151 .filter(
5152 &_key,
5153 #root::core::SqlValue::String(pattern.into()),
5154 )
5155 .fetch_pool(pool)
5156 .await
5157 }
5158
5159 /// Fetch every row where `<col>` starts with `prefix`
5160 /// (auto-appends `%`). Django `__startswith` / Eloquent
5161 /// `whereLike("col", "$prefix%")` parity.
5162 ///
5163 /// # Errors
5164 /// As [`FetcherPool::fetch_pool`].
5165 ///
5166 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5167 pub async fn where_starts_with(
5168 col: &str,
5169 prefix: impl ::core::convert::Into<::std::string::String>,
5170 pool: &#root::sql::Pool,
5171 ) -> ::core::result::Result<
5172 ::std::vec::Vec<Self>,
5173 #root::sql::ExecError,
5174 > {
5175 use #root::sql::FetcherPool as _;
5176 let _key = ::std::format!("{}__startswith", col);
5177 #root::query::QuerySet::<Self>::default()
5178 .filter(
5179 &_key,
5180 #root::core::SqlValue::String(prefix.into()),
5181 )
5182 .fetch_pool(pool)
5183 .await
5184 }
5185
5186 /// Fetch every row where `<col>` ends with `suffix`
5187 /// (auto-prepends `%`). Django `__endswith` / Eloquent
5188 /// `whereLike("col", "%$suffix")` parity.
5189 ///
5190 /// # Errors
5191 /// As [`FetcherPool::fetch_pool`].
5192 ///
5193 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5194 pub async fn where_ends_with(
5195 col: &str,
5196 suffix: impl ::core::convert::Into<::std::string::String>,
5197 pool: &#root::sql::Pool,
5198 ) -> ::core::result::Result<
5199 ::std::vec::Vec<Self>,
5200 #root::sql::ExecError,
5201 > {
5202 use #root::sql::FetcherPool as _;
5203 let _key = ::std::format!("{}__endswith", col);
5204 #root::query::QuerySet::<Self>::default()
5205 .filter(
5206 &_key,
5207 #root::core::SqlValue::String(suffix.into()),
5208 )
5209 .fetch_pool(pool)
5210 .await
5211 }
5212
5213 /// Fetch every row where `<col>` contains `substr`
5214 /// (auto-wraps with `%`). Django `__contains` /
5215 /// Eloquent `whereLike("col", "%$substr%")` parity.
5216 ///
5217 /// # Errors
5218 /// As [`FetcherPool::fetch_pool`].
5219 ///
5220 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5221 pub async fn where_contains(
5222 col: &str,
5223 substr: impl ::core::convert::Into<::std::string::String>,
5224 pool: &#root::sql::Pool,
5225 ) -> ::core::result::Result<
5226 ::std::vec::Vec<Self>,
5227 #root::sql::ExecError,
5228 > {
5229 use #root::sql::FetcherPool as _;
5230 let _key = ::std::format!("{}__contains", col);
5231 #root::query::QuerySet::<Self>::default()
5232 .filter(
5233 &_key,
5234 #root::core::SqlValue::String(substr.into()),
5235 )
5236 .fetch_pool(pool)
5237 .await
5238 }
5239
5240 /// Fetch every row where `<col> > val`. Eloquent
5241 /// `Model::where($col, ">", $val)->get()` parity.
5242 ///
5243 /// # Errors
5244 /// As [`FetcherPool::fetch_pool`].
5245 ///
5246 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5247 pub async fn where_gt(
5248 col: &str,
5249 val: impl ::core::convert::Into<#root::core::SqlValue>,
5250 pool: &#root::sql::Pool,
5251 ) -> ::core::result::Result<
5252 ::std::vec::Vec<Self>,
5253 #root::sql::ExecError,
5254 > {
5255 use #root::sql::FetcherPool as _;
5256 let _key = ::std::format!("{}__gt", col);
5257 #root::query::QuerySet::<Self>::default()
5258 .filter(&_key, val)
5259 .fetch_pool(pool)
5260 .await
5261 }
5262
5263 /// Fetch every row where `<col> >= val`. Eloquent
5264 /// `Model::where($col, ">=", $val)->get()` parity.
5265 ///
5266 /// # Errors
5267 /// As [`FetcherPool::fetch_pool`].
5268 ///
5269 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5270 pub async fn where_gte(
5271 col: &str,
5272 val: impl ::core::convert::Into<#root::core::SqlValue>,
5273 pool: &#root::sql::Pool,
5274 ) -> ::core::result::Result<
5275 ::std::vec::Vec<Self>,
5276 #root::sql::ExecError,
5277 > {
5278 use #root::sql::FetcherPool as _;
5279 let _key = ::std::format!("{}__gte", col);
5280 #root::query::QuerySet::<Self>::default()
5281 .filter(&_key, val)
5282 .fetch_pool(pool)
5283 .await
5284 }
5285
5286 /// Fetch every row where `<col> < val`. Eloquent
5287 /// `Model::where($col, "<", $val)->get()` parity.
5288 ///
5289 /// # Errors
5290 /// As [`FetcherPool::fetch_pool`].
5291 ///
5292 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5293 pub async fn where_lt(
5294 col: &str,
5295 val: impl ::core::convert::Into<#root::core::SqlValue>,
5296 pool: &#root::sql::Pool,
5297 ) -> ::core::result::Result<
5298 ::std::vec::Vec<Self>,
5299 #root::sql::ExecError,
5300 > {
5301 use #root::sql::FetcherPool as _;
5302 let _key = ::std::format!("{}__lt", col);
5303 #root::query::QuerySet::<Self>::default()
5304 .filter(&_key, val)
5305 .fetch_pool(pool)
5306 .await
5307 }
5308
5309 /// Fetch every row where `<col> <= val`. Eloquent
5310 /// `Model::where($col, "<=", $val)->get()` parity.
5311 ///
5312 /// # Errors
5313 /// As [`FetcherPool::fetch_pool`].
5314 ///
5315 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5316 pub async fn where_lte(
5317 col: &str,
5318 val: impl ::core::convert::Into<#root::core::SqlValue>,
5319 pool: &#root::sql::Pool,
5320 ) -> ::core::result::Result<
5321 ::std::vec::Vec<Self>,
5322 #root::sql::ExecError,
5323 > {
5324 use #root::sql::FetcherPool as _;
5325 let _key = ::std::format!("{}__lte", col);
5326 #root::query::QuerySet::<Self>::default()
5327 .filter(&_key, val)
5328 .fetch_pool(pool)
5329 .await
5330 }
5331
5332 /// Fetch every row where `<col> <> val`. Eloquent
5333 /// `Model::where($col, "!=", $val)->get()` parity.
5334 ///
5335 /// # Errors
5336 /// As [`FetcherPool::fetch_pool`].
5337 ///
5338 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5339 pub async fn where_ne(
5340 col: &str,
5341 val: impl ::core::convert::Into<#root::core::SqlValue>,
5342 pool: &#root::sql::Pool,
5343 ) -> ::core::result::Result<
5344 ::std::vec::Vec<Self>,
5345 #root::sql::ExecError,
5346 > {
5347 use #root::sql::FetcherPool as _;
5348 let _key = ::std::format!("{}__ne", col);
5349 #root::query::QuerySet::<Self>::default()
5350 .filter(&_key, val)
5351 .fetch_pool(pool)
5352 .await
5353 }
5354
5355 /// Fetch every row matching `<col> = val` for ANY of the
5356 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5357 /// parity. Empty `cols` returns no rows.
5358 ///
5359 /// Resolves each `&str` column to its SCHEMA-registered
5360 /// `&'static str` once and builds a single OR-composed
5361 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5362 ///
5363 /// # Errors
5364 /// As [`FetcherPool::fetch_pool`]; `QueryError::UnknownField`
5365 /// when any column is not declared on the model.
5366 ///
5367 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5368 pub async fn where_any(
5369 cols: &[&str],
5370 val: impl ::core::convert::Into<#root::core::SqlValue>,
5371 pool: &#root::sql::Pool,
5372 ) -> ::core::result::Result<
5373 ::std::vec::Vec<Self>,
5374 #root::sql::ExecError,
5375 > {
5376 Self::__where_multi(cols, val, false, pool).await
5377 }
5378
5379 /// Fetch every row matching `<col> = val` for ALL listed
5380 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5381 /// Empty `cols` returns every row (vacuous AND).
5382 ///
5383 /// # Errors
5384 /// As [`Self::where_any`].
5385 pub async fn where_all(
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, true, pool).await
5394 }
5395
5396 /// Internal: build a Q expression composing `cols` via
5397 /// AND (`all`) or OR (`!all`), then fetch. Backs
5398 /// `where_any` / `where_all`.
5399 #[doc(hidden)]
5400 pub async fn __where_multi(
5401 cols: &[&str],
5402 val: impl ::core::convert::Into<#root::core::SqlValue>,
5403 all: bool,
5404 pool: &#root::sql::Pool,
5405 ) -> ::core::result::Result<
5406 ::std::vec::Vec<Self>,
5407 #root::sql::ExecError,
5408 > {
5409 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5410 .await
5411 }
5412
5413 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5414 /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5415 /// is NOT guaranteed without an explicit `order_by` —
5416 /// drop into `Self::query()` for that.
5417 ///
5418 /// # Errors
5419 /// As [`FetcherPool::fetch_pool`].
5420 ///
5421 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5422 pub async fn take(
5423 n: i64,
5424 pool: &#root::sql::Pool,
5425 ) -> ::core::result::Result<
5426 ::std::vec::Vec<Self>,
5427 #root::sql::ExecError,
5428 > {
5429 use #root::sql::FetcherPool as _;
5430 #root::query::QuerySet::<Self>::default()
5431 .limit(n)
5432 .fetch_pool(pool)
5433 .await
5434 }
5435
5436 /// Fetch the page-th window of `per_page` rows
5437 /// (1-indexed). Eloquent
5438 /// `Model::query()->forPage($page, $perPage)->get()`
5439 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5440 /// LIMIT per_page`; for large offsets this is O(N) —
5441 /// prefer keyset pagination via PK on hot paths.
5442 ///
5443 /// # Errors
5444 /// As [`FetcherPool::fetch_pool`].
5445 ///
5446 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5447 pub async fn for_page(
5448 page: i64,
5449 per_page: i64,
5450 pool: &#root::sql::Pool,
5451 ) -> ::core::result::Result<
5452 ::std::vec::Vec<Self>,
5453 #root::sql::ExecError,
5454 > {
5455 use #root::sql::FetcherPool as _;
5456 let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5457 #root::query::QuerySet::<Self>::default()
5458 .limit(per_page)
5459 .offset(_offset)
5460 .fetch_pool(pool)
5461 .await
5462 }
5463
5464 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5465 /// one page of rows AND the total row count in one
5466 /// call. Returns `(rows, total)`. Two queries: a
5467 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5468 /// for the total.
5469 ///
5470 /// Useful for paginated UIs that need both the visible
5471 /// rows AND a "Page X of Y" / total-count widget. For
5472 /// large tables prefer keyset pagination + cached count;
5473 /// every call to `paginate` re-counts the full table.
5474 ///
5475 /// Same 1-indexed `page` convention as [`Self::for_page`].
5476 ///
5477 /// # Errors
5478 /// As [`Self::for_page`] and [`Self::count`].
5479 pub async fn paginate(
5480 page: i64,
5481 per_page: i64,
5482 pool: &#root::sql::Pool,
5483 ) -> ::core::result::Result<
5484 (::std::vec::Vec<Self>, i64),
5485 #root::sql::ExecError,
5486 > {
5487 // Route through the queryset rather than `Self::count`:
5488 // the `count` inherent method is suppressed on models
5489 // that declare a field named `count` (field/shortcut
5490 // collision guard), and `paginate` must still compile
5491 // for those models.
5492 let total = {
5493 use #root::sql::CounterPool as _;
5494 #root::query::QuerySet::<Self>::default()
5495 .count_pool(pool)
5496 .await?
5497 };
5498 let rows = Self::for_page(page, per_page, pool).await?;
5499 ::core::result::Result::Ok((rows, total))
5500 }
5501
5502 /// Bulk-update — set `set_col = set_val` on every row
5503 /// matching `where_col = where_val`. Returns affected row
5504 /// count. Eloquent
5505 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5506 /// parity.
5507 ///
5508 /// For multi-column updates drop into the queryset
5509 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5510 ///
5511 /// # Errors
5512 /// As [`UpdaterPool::execute_pool`].
5513 ///
5514 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5515 pub async fn update_where(
5516 where_col: &str,
5517 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5518 set_col: &str,
5519 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5520 pool: &#root::sql::Pool,
5521 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5522 use #root::sql::UpdaterPool as _;
5523 #root::query::QuerySet::<Self>::default()
5524 .filter(where_col, where_val)
5525 .update()
5526 .set(set_col, set_val)
5527 .execute_pool(pool)
5528 .await
5529 }
5530
5531 /// Bulk-delete — remove every row matching
5532 /// `where_col = where_val`. Returns affected row count.
5533 /// Eloquent
5534 /// `Model::where($where_col, $where_val)->delete()` parity.
5535 ///
5536 /// For more complex filters drop into the queryset
5537 /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5538 ///
5539 /// # Errors
5540 /// As [`#root::sql::delete_pool`].
5541 pub async fn delete_where(
5542 where_col: &str,
5543 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5544 pool: &#root::sql::Pool,
5545 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5546 let _query = #root::core::DeleteQuery {
5547 model: <Self as #root::core::Model>::SCHEMA,
5548 where_clause: #root::core::WhereExpr::Predicate(
5549 #root::core::Filter {
5550 column: <Self as #root::core::Model>::SCHEMA
5551 .field(where_col)
5552 .ok_or_else(|| {
5553 #root::sql::ExecError::Query(
5554 #root::core::QueryError::UnknownField {
5555 model: <Self as #root::core::Model>::SCHEMA.name,
5556 field: ::std::string::ToString::to_string(where_col),
5557 },
5558 )
5559 })?
5560 .column,
5561 op: #root::core::Op::Eq,
5562 value: ::core::convert::Into::into(where_val),
5563 },
5564 ),
5565 };
5566 #root::sql::delete_pool(pool, &_query).await
5567 }
5568
5569 /// Bulk-update — set `set_col = set_val` on EVERY row of
5570 /// this model's table. **No WHERE clause** — use with
5571 /// care. Eloquent `Model::query()->update([$col => $val])`
5572 /// parity.
5573 ///
5574 /// Use for backfills, one-shot reset flows, etc. The
5575 /// returned count is rows affected.
5576 ///
5577 /// # Errors
5578 /// As [`UpdaterPool::execute_pool`].
5579 ///
5580 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5581 pub async fn update_all(
5582 set_col: &str,
5583 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5584 pool: &#root::sql::Pool,
5585 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5586 use #root::sql::UpdaterPool as _;
5587 #root::query::QuerySet::<Self>::default()
5588 .update()
5589 .set(set_col, set_val)
5590 .execute_pool(pool)
5591 .await
5592 }
5593
5594 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5595 /// Eloquent `Model::whereNotLike` parity. Pattern is
5596 /// passed verbatim — caller controls `%` / `_`.
5597 ///
5598 /// # Errors
5599 /// As [`FetcherPool::fetch_pool`].
5600 ///
5601 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5602 pub async fn where_not_like(
5603 col: &str,
5604 pattern: impl ::core::convert::Into<::std::string::String>,
5605 pool: &#root::sql::Pool,
5606 ) -> ::core::result::Result<
5607 ::std::vec::Vec<Self>,
5608 #root::sql::ExecError,
5609 > {
5610 use #root::sql::FetcherPool as _;
5611 let _key = ::std::format!("{}__not_like", col);
5612 #root::query::QuerySet::<Self>::default()
5613 .filter(
5614 &_key,
5615 #root::core::SqlValue::String(pattern.into()),
5616 )
5617 .fetch_pool(pool)
5618 .await
5619 }
5620
5621 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5622 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5623 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5624 ///
5625 /// # Errors
5626 /// As [`FetcherPool::fetch_pool`].
5627 ///
5628 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5629 pub async fn where_not_ilike(
5630 col: &str,
5631 pattern: impl ::core::convert::Into<::std::string::String>,
5632 pool: &#root::sql::Pool,
5633 ) -> ::core::result::Result<
5634 ::std::vec::Vec<Self>,
5635 #root::sql::ExecError,
5636 > {
5637 use #root::sql::FetcherPool as _;
5638 let _key = ::std::format!("{}__not_ilike", col);
5639 #root::query::QuerySet::<Self>::default()
5640 .filter(
5641 &_key,
5642 #root::core::SqlValue::String(pattern.into()),
5643 )
5644 .fetch_pool(pool)
5645 .await
5646 }
5647
5648 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5649 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5650 /// parity.
5651 ///
5652 /// # Errors
5653 /// As [`FetcherPool::fetch_pool`].
5654 ///
5655 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5656 pub async fn where_not_between(
5657 col: &str,
5658 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5659 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5660 pool: &#root::sql::Pool,
5661 ) -> ::core::result::Result<
5662 ::std::vec::Vec<Self>,
5663 #root::sql::ExecError,
5664 > {
5665 use #root::sql::FetcherPool as _;
5666 let _key = ::std::format!("{}__not_between", col);
5667 let _vals = #root::core::SqlValue::List(::std::vec![
5668 ::core::convert::Into::into(lo),
5669 ::core::convert::Into::into(hi),
5670 ]);
5671 #root::query::QuerySet::<Self>::default()
5672 .filter(&_key, _vals)
5673 .fetch_pool(pool)
5674 .await
5675 }
5676
5677 /// Returns the SQL table name for this model. Eloquent
5678 /// `$model->getTable()` parity.
5679 #[must_use]
5680 pub fn table_name() -> &'static str {
5681 <Self as #root::core::Model>::SCHEMA.table
5682 }
5683
5684 /// Returns the SQL column name of this model's primary
5685 /// key, or `None` when the model has no
5686 /// `#[rustango(primary_key)]`. Eloquent
5687 /// `$model->getKeyName()` parity.
5688 #[must_use]
5689 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5690 <Self as #root::core::Model>::SCHEMA
5691 .primary_key()
5692 .map(|f| f.column)
5693 }
5694
5695 /// Fetch every row where `<col> BETWEEN lo AND hi`
5696 /// (inclusive on both ends — same as SQL). Eloquent
5697 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5698 ///
5699 /// # Errors
5700 /// As [`FetcherPool::fetch_pool`].
5701 ///
5702 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5703 pub async fn where_between(
5704 col: &str,
5705 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5706 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5707 pool: &#root::sql::Pool,
5708 ) -> ::core::result::Result<
5709 ::std::vec::Vec<Self>,
5710 #root::sql::ExecError,
5711 > {
5712 use #root::sql::FetcherPool as _;
5713 let _key = ::std::format!("{}__between", col);
5714 let _vals = #root::core::SqlValue::List(::std::vec![
5715 ::core::convert::Into::into(lo),
5716 ::core::convert::Into::into(hi),
5717 ]);
5718 #root::query::QuerySet::<Self>::default()
5719 .filter(&_key, _vals)
5720 .fetch_pool(pool)
5721 .await
5722 }
5723
5724 /// Fetch the first row where `<col> = <val>`. Returns
5725 /// `Ok(None)` when no row matches. Eloquent
5726 /// `Model::firstWhere($col, $val)` / Django
5727 /// `Model.objects.filter(col=val).first()` parity.
5728 ///
5729 /// Thin wrapper over `QuerySet::<Self>::default()
5730 /// .filter(col, val).first(pool)`. Use this when you
5731 /// want one row identified by a non-PK column (e.g.
5732 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5733 ///
5734 /// `val` accepts any value `Into<SqlValue>` so plain
5735 /// strings, ints, UUIDs, etc. all work.
5736 ///
5737 /// # Errors
5738 /// As `QuerySet::first`.
5739 pub async fn first_where(
5740 col: &str,
5741 val: impl ::core::convert::Into<#root::core::SqlValue>,
5742 pool: &#root::sql::Pool,
5743 ) -> ::core::result::Result<
5744 ::core::option::Option<Self>,
5745 #root::sql::ExecError,
5746 > {
5747 #root::query::QuerySet::<Self>::default()
5748 .filter(col, val)
5749 .first(pool)
5750 .await
5751 }
5752
5753 #value_method
5754
5755 /// Fetch the row with the largest `field` value —
5756 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5757 /// `Ok(None)` for an empty table. Eloquent
5758 /// `Model::latest($field)->first()` / Django
5759 /// `Model.objects.latest(field)` (non-throwing) parity.
5760 /// Thin wrapper over `QuerySet::<Self>::default()
5761 /// .latest(field, pool)`.
5762 ///
5763 /// **Field name** is the Rust field ident as a string
5764 /// (not the SQL column). Unknown fields surface as
5765 /// `ExecError::Query(QueryError::UnknownField)` at
5766 /// compile time.
5767 ///
5768 /// # Errors
5769 /// As `QuerySet::latest`.
5770 pub async fn latest(
5771 field: &str,
5772 pool: &#root::sql::Pool,
5773 ) -> ::core::result::Result<
5774 ::core::option::Option<Self>,
5775 #root::sql::ExecError,
5776 > {
5777 #root::query::QuerySet::<Self>::default()
5778 .latest(field, pool)
5779 .await
5780 }
5781
5782 /// Sibling of [`Self::latest_pool`] — fetches the row
5783 /// with the smallest `field` value (`ORDER BY <field>
5784 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5785 /// ->first()` / Django `Model.objects.earliest(field)`
5786 /// parity.
5787 ///
5788 /// # Errors
5789 /// As [`Self::latest_pool`].
5790 pub async fn earliest(
5791 field: &str,
5792 pool: &#root::sql::Pool,
5793 ) -> ::core::result::Result<
5794 ::core::option::Option<Self>,
5795 #root::sql::ExecError,
5796 > {
5797 #root::query::QuerySet::<Self>::default()
5798 .earliest(field, pool)
5799 .await
5800 }
5801
5802 #count_method
5803
5804 /// `true` when the table contains at least one row.
5805 /// Eloquent `Model::query()->exists()` / Django
5806 /// `Model.objects.exists()` parity. Thin wrapper over
5807 /// `QuerySet::<Self>::default().exists_pool(pool)`.
5808 ///
5809 /// # Errors
5810 /// As [`ExistsPool::exists_pool`].
5811 ///
5812 /// [`ExistsPool::exists_pool`]: rustango::sql::ExistsPool::exists_pool
5813 pub async fn exists(
5814 pool: &#root::sql::Pool,
5815 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5816 use #root::sql::ExistsPool as _;
5817 #root::query::QuerySet::<Self>::default()
5818 .exists_pool(pool)
5819 .await
5820 }
5821
5822 /// Inverse of [`Self::exists_pool`] — returns `true` when
5823 /// the table has zero rows. Eloquent
5824 /// `Model::doesntExist()` parity.
5825 ///
5826 /// # Errors
5827 /// As [`Self::exists_pool`].
5828 pub async fn doesnt_exist(
5829 pool: &#root::sql::Pool,
5830 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5831 Self::exists(pool).await.map(|e| !e)
5832 }
5833
5834 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5835 /// `true` when a row with primary key `pk` exists in
5836 /// the table. Sugar over
5837 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5838 ///
5839 /// Differs from [`Self::exists`] (which checks "any row
5840 /// in the table") by checking a specific PK existence.
5841 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5842 /// because the row is never materialized — the SQL is
5843 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5844 ///
5845 /// # Errors
5846 /// As [`#root::sql::ExistsPool::contains_pk`].
5847 pub async fn contains_pk(
5848 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5849 pool: &#root::sql::Pool,
5850 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5851 use #root::sql::ExistsPool as _;
5852 #root::query::QuerySet::<Self>::default()
5853 .contains_pk(pool, pk)
5854 .await
5855 }
5856
5857 #sum_method
5858 #avg_method
5859 #min_method
5860 #max_method
5861
5862 /// Internal: forward to
5863 /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5864 /// Backs `sum` / `avg` / `min` / `max`.
5865 #[doc(hidden)]
5866 pub async fn __aggregate_one_pool<U>(
5867 col: &str,
5868 build: fn(&'static str) -> #root::core::AggregateExpr,
5869 pool: &#root::sql::Pool,
5870 ) -> ::core::result::Result<
5871 ::core::option::Option<U>,
5872 #root::sql::ExecError,
5873 >
5874 where
5875 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5876 + #root::sql::MaybeMyFromRow
5877 + #root::sql::MaybeSqliteFromRow
5878 + ::core::marker::Send
5879 + ::core::marker::Unpin,
5880 {
5881 #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5882 .await
5883 }
5884
5885 /// Fetch every row of this model from `pool`. Eloquent
5886 /// `Model::all()` parity — a thin wrapper over
5887 /// `QuerySet::<Self>::default().fetch_pool(pool)`.
5888 ///
5889 /// **Use with care on large tables** — there's no
5890 /// pagination or limit; the entire table is materialized
5891 /// into memory. For anything beyond fixture / lookup
5892 /// tables, page through `QuerySet::<Self>::default()
5893 /// .order_by(...).limit(N).offset(M).fetch_pool(pool)`
5894 /// or stream via `.iterator(chunk_size)`.
5895 ///
5896 /// # Errors
5897 /// As [`FetcherPool::fetch_pool`].
5898 ///
5899 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5900 pub async fn all(
5901 pool: &#root::sql::Pool,
5902 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5903 {
5904 use #root::sql::FetcherPool as _;
5905 #root::query::QuerySet::<Self>::default()
5906 .fetch_pool(pool)
5907 .await
5908 }
5909
5910 /// Look up every row whose primary key is in `pks`.
5911 /// Returns the matching rows in **inventory** order — NOT
5912 /// the order of `pks`. Empty `pks` returns an empty
5913 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5914 /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5915 /// parity.
5916 ///
5917 /// Thin wrapper over `QuerySet::<Self>::default()
5918 /// .filter("<pk>__in", SqlValue::List([...])).fetch_pool(pool)`.
5919 /// Caller-supplied PKs that don't match a row are
5920 /// silently skipped (the returned vec is shorter than
5921 /// the input list). For an order-preserving / "fail
5922 /// when any missing" variant, build the queryset
5923 /// explicitly with `in_bulk` instead.
5924 ///
5925 /// Accepts any iterable whose elements are
5926 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5927 /// `Vec<Uuid>`, etc.
5928 ///
5929 /// # Errors
5930 /// As [`FetcherPool::fetch_pool`].
5931 ///
5932 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5933 pub async fn find_many<V>(
5934 pks: impl ::core::iter::IntoIterator<Item = V>,
5935 pool: &#root::sql::Pool,
5936 ) -> ::core::result::Result<
5937 ::std::vec::Vec<Self>,
5938 #root::sql::ExecError,
5939 >
5940 where
5941 V: ::core::convert::Into<#root::core::SqlValue>,
5942 {
5943 use #root::sql::FetcherPool as _;
5944 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5945 pks.into_iter().map(::core::convert::Into::into).collect();
5946 if _values.is_empty() {
5947 return ::core::result::Result::Ok(::std::vec::Vec::new());
5948 }
5949 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5950 #root::query::QuerySet::<Self>::default()
5951 .filter(&_key, #root::core::SqlValue::List(_values))
5952 .fetch_pool(pool)
5953 .await
5954 }
5955
5956 /// Look up the row whose primary key equals `pk`. Returns
5957 /// `Ok(None)` when no row matches; this is the
5958 /// non-throwing counterpart of Django's `.get(pk=…)`
5959 /// (which raises `DoesNotExist`). Eloquent `Model::find`
5960 /// shape — accepts any value `Into<SqlValue>`.
5961 ///
5962 /// One-liner shortcut for the common
5963 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
5964 /// .limit(1).fetch_pool(pool).await?.into_iter().next()`
5965 /// dance.
5966 ///
5967 /// # Errors
5968 /// As [`FetcherPool::fetch_pool`].
5969 ///
5970 /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5971 pub async fn find(
5972 pk: impl ::core::convert::Into<#root::core::SqlValue>,
5973 pool: &#root::sql::Pool,
5974 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
5975 {
5976 use #root::sql::FetcherPool as _;
5977 let _pk_val: #root::core::SqlValue = pk.into();
5978 let mut _rows: ::std::vec::Vec<Self> =
5979 #root::query::QuerySet::<Self>::default()
5980 .filter(::core::stringify!(#pk_ident), _pk_val)
5981 .limit(1)
5982 .fetch_pool(pool)
5983 .await?;
5984 ::core::result::Result::Ok(_rows.into_iter().next())
5985 }
5986
5987 /// Look up the row whose primary key equals `pk`. Errors
5988 /// when no row matches — the throwing counterpart of
5989 /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
5990 /// Django `Model.objects.get(pk=…)` (which raises
5991 /// `DoesNotExist`) parity.
5992 ///
5993 /// Translates the miss into
5994 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
5995 /// so callers can `?`-bubble straight through the typical
5996 /// `ExecError` error chain.
5997 ///
5998 /// # Errors
5999 /// As [`Self::find_pool`]; additionally
6000 /// [`sqlx::Error::RowNotFound`] when no row matches.
6001 ///
6002 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6003 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6004 pub async fn find_or_fail(
6005 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6006 pool: &#root::sql::Pool,
6007 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6008 match Self::find(pk, pool).await? {
6009 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6010 ::core::option::Option::None => ::core::result::Result::Err(
6011 #root::sql::ExecError::Driver(
6012 #root::sql::sqlx::Error::RowNotFound,
6013 ),
6014 ),
6015 }
6016 }
6017
6018 /// Look up multiple rows by primary key, **failing** if
6019 /// any requested PK is missing. Eloquent
6020 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6021 /// inventory order (NOT request order — same as
6022 /// [`Self::find_many`]).
6023 ///
6024 /// Empty `pks` returns an empty `Vec` (no rows requested
6025 /// → nothing to fail on).
6026 ///
6027 /// Differs from [`Self::find_many`] in that this method
6028 /// requires every PK to resolve. If even one is missing,
6029 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6030 /// in `ExecError::Driver`). Rows are deduped at the SQL
6031 /// layer, so passing the same PK twice in `pks` counts as
6032 /// one expected row.
6033 ///
6034 /// # Errors
6035 /// As [`Self::find_many`], plus `RowNotFound` when the
6036 /// returned row count is less than the count of distinct
6037 /// requested PKs.
6038 pub async fn find_many_or_fail<V>(
6039 pks: impl ::core::iter::IntoIterator<Item = V>,
6040 pool: &#root::sql::Pool,
6041 ) -> ::core::result::Result<
6042 ::std::vec::Vec<Self>,
6043 #root::sql::ExecError,
6044 >
6045 where
6046 V: ::core::convert::Into<#root::core::SqlValue>,
6047 {
6048 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6049 pks.into_iter().map(::core::convert::Into::into).collect();
6050 if _values.is_empty() {
6051 return ::core::result::Result::Ok(::std::vec::Vec::new());
6052 }
6053 // Dedup the requested PK list before counting so
6054 // duplicate-PK requests don't false-fail (the SQL
6055 // `IN (...)` clause naturally dedups too).
6056 let mut _seen: ::std::collections::HashSet<
6057 ::std::string::String,
6058 > = ::std::collections::HashSet::new();
6059 for v in &_values {
6060 _seen.insert(v.to_display_string());
6061 }
6062 let _expected = _seen.len();
6063 let _rows = Self::find_many(_values, pool).await?;
6064 if _rows.len() < _expected {
6065 return ::core::result::Result::Err(
6066 #root::sql::ExecError::Driver(
6067 #root::sql::sqlx::Error::RowNotFound,
6068 ),
6069 );
6070 }
6071 ::core::result::Result::Ok(_rows)
6072 }
6073
6074 /// Find by primary key or run `fallback` to produce a
6075 /// default row to return. Eloquent
6076 /// `Model::findOr($pk, fn() => …)` parity.
6077 ///
6078 /// Unlike [`Self::find_or_fail_pool`] (which raises on
6079 /// miss), this is the "give me something sensible"
6080 /// branch: typical use is "fetch the user's row, else
6081 /// fall back to an anonymous/guest stub".
6082 ///
6083 /// `fallback` runs only when no row matches — the DB
6084 /// round-trip happens unconditionally.
6085 ///
6086 /// # Errors
6087 /// As [`Self::find_pool`].
6088 pub async fn find_or<F>(
6089 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6090 pool: &#root::sql::Pool,
6091 fallback: F,
6092 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6093 where
6094 F: ::core::ops::FnOnce() -> Self,
6095 {
6096 ::core::result::Result::Ok(
6097 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6098 )
6099 }
6100
6101 /// Same as [`Self::find_or`] but also returns a `bool`
6102 /// indicating whether the row was found in the DB
6103 /// (`true`) or freshly built from `fallback` (`false`).
6104 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6105 /// in PHP the returned model also exposes `->exists`;
6106 /// here that's surfaced as the second tuple element.
6107 ///
6108 /// Useful for edit-or-create form handlers where the
6109 /// caller needs to know whether to PATCH or POST when
6110 /// the user submits the form.
6111 ///
6112 /// # Errors
6113 /// As [`Self::find`].
6114 pub async fn find_or_new<F>(
6115 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6116 pool: &#root::sql::Pool,
6117 fallback: F,
6118 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6119 where
6120 F: ::core::ops::FnOnce() -> Self,
6121 {
6122 match Self::find(pk, pool).await? {
6123 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6124 ::core::option::Option::None => {
6125 ::core::result::Result::Ok((fallback(), false))
6126 }
6127 }
6128 }
6129
6130 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6131 /// [`Self::find_or_new`] but **persists** the new row
6132 /// when the PK isn't found. Returns `(row, exists: bool)`
6133 /// — `exists=true` when the PK was found,
6134 /// `false` when a fresh row was inserted.
6135 ///
6136 /// ```ignore
6137 /// let (post, found) = Post::find_or_insert(
6138 /// pk,
6139 /// &pool,
6140 /// || Post { id: Auto::default(), title: "new".into() },
6141 /// ).await?;
6142 /// // `found` true → returned existing row;
6143 /// // `found` false → fallback was inserted, `post.id` populated.
6144 /// ```
6145 ///
6146 /// **Caveat**: two concurrent calls that both miss the
6147 /// find can both INSERT, violating uniqueness. Wrap in a
6148 /// transaction or rely on a UNIQUE constraint + handle
6149 /// the conflict error if you need race-free semantics.
6150 ///
6151 /// # Errors
6152 /// As [`Self::find`] and [`Self::save_pool`].
6153 pub async fn find_or_insert<F>(
6154 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6155 pool: &#root::sql::Pool,
6156 fallback: F,
6157 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6158 where
6159 F: ::core::ops::FnOnce() -> Self,
6160 {
6161 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6162 return ::core::result::Result::Ok((_row, true));
6163 }
6164 let mut _new = fallback();
6165 _new.save_pool(pool).await?;
6166 ::core::result::Result::Ok((_new, false))
6167 }
6168
6169 /// Fetch the first row of the table, or run `fallback`
6170 /// when the table is empty. Eloquent
6171 /// `Model::firstOr(fn() => …)` parity.
6172 ///
6173 /// # Errors
6174 /// As [`Self::first_pool`].
6175 pub async fn first_or<F>(
6176 pool: &#root::sql::Pool,
6177 fallback: F,
6178 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6179 where
6180 F: ::core::ops::FnOnce() -> Self,
6181 {
6182 // Route through the queryset rather than `Self::first`,
6183 // which is suppressed on models with a field named
6184 // `first` (field/shortcut collision guard).
6185 ::core::result::Result::Ok(
6186 #root::query::QuerySet::<Self>::default()
6187 .first(pool)
6188 .await?
6189 .unwrap_or_else(fallback),
6190 )
6191 }
6192
6193 /// Fetch exactly one row matching `<col> = <val>`. Errors
6194 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6195 /// or more than one matches
6196 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6197 /// Eloquent `Model::sole($col, $val)` parity.
6198 ///
6199 /// # Errors
6200 /// As [`Self::where_pool`] plus the explicit
6201 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6202 pub async fn sole(
6203 col: &str,
6204 val: impl ::core::convert::Into<#root::core::SqlValue>,
6205 pool: &#root::sql::Pool,
6206 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6207 let mut _rows = Self::where_(col, val, pool).await?;
6208 match _rows.len() {
6209 0 => ::core::result::Result::Err(
6210 #root::sql::ExecError::Driver(
6211 #root::sql::sqlx::Error::RowNotFound,
6212 ),
6213 ),
6214 1 => ::core::result::Result::Ok(_rows.remove(0)),
6215 n => ::core::result::Result::Err(
6216 #root::sql::ExecError::MultipleRowsReturned {
6217 op: "sole",
6218 table: <Self as #root::core::Model>::SCHEMA.name,
6219 count: n,
6220 },
6221 ),
6222 }
6223 }
6224 }
6225 } else {
6226 quote!()
6227 };
6228
6229 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6230 // the non-audited `_pool` methods but execute against an open
6231 // `PoolTx` so the writes participate in the caller's transaction.
6232 // Auditing inside TX is deferred; these always use the plain
6233 // executor primitives regardless of whether the model is audited.
6234 let tx_insert_method = if fields.has_auto {
6235 let pushes = &fields.insert_pushes;
6236 let returning_cols = &fields.returning_cols;
6237 quote! {
6238 /// Insert this row inside an open transaction, populating
6239 /// any `Auto<T>` PK from the auto-assigned value. Works
6240 /// against any backend that `tx` wraps.
6241 ///
6242 /// # Errors
6243 /// As [`Self::insert_pool`].
6244 pub async fn insert_tx(
6245 &mut self,
6246 tx: &mut #root::sql::PoolTx<'_>,
6247 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6248 let mut _columns: ::std::vec::Vec<&'static str> =
6249 ::std::vec::Vec::new();
6250 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6251 ::std::vec::Vec::new();
6252 #( #pushes )*
6253 let _query = #root::core::InsertQuery {
6254 model: <Self as #root::core::Model>::SCHEMA,
6255 columns: _columns,
6256 values: _values,
6257 returning: ::std::vec![ #( #returning_cols ),* ],
6258 on_conflict: ::core::option::Option::None,
6259 };
6260 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6261 #root::sql::apply_auto_pk(_result, self)
6262 }
6263 }
6264 } else {
6265 let insert_columns = &fields.insert_columns;
6266 let insert_values = &fields.insert_values;
6267 quote! {
6268 /// Insert this row inside an open transaction.
6269 ///
6270 /// # Errors
6271 /// As [`Self::insert_pool`].
6272 pub async fn insert_tx(
6273 &self,
6274 tx: &mut #root::sql::PoolTx<'_>,
6275 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6276 let _query = #root::core::InsertQuery {
6277 model: <Self as #root::core::Model>::SCHEMA,
6278 columns: ::std::vec![ #( #insert_columns ),* ],
6279 values: ::std::vec![ #( #insert_values ),* ],
6280 returning: ::std::vec::Vec::new(),
6281 on_conflict: ::core::option::Option::None,
6282 };
6283 #root::sql::insert_tx(tx, &_query).await
6284 }
6285 }
6286 };
6287
6288 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6289 let pk_column_lit = pk_col.as_str();
6290 let assignments = &fields.update_assignments;
6291 let dispatch_unset = if fields.pk_is_auto {
6292 quote! {
6293 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6294 return self.insert_tx(tx).await;
6295 }
6296 }
6297 } else {
6298 quote!()
6299 };
6300 quote! {
6301 /// Save this row inside an open transaction. `INSERT` when
6302 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6303 /// PK. Works against any backend that `tx` wraps.
6304 ///
6305 /// # Errors
6306 /// As [`Self::save_pool`].
6307 pub async fn save_tx(
6308 &mut self,
6309 tx: &mut #root::sql::PoolTx<'_>,
6310 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6311 #dispatch_unset
6312 let _query = #root::core::UpdateQuery {
6313 model: <Self as #root::core::Model>::SCHEMA,
6314 set: ::std::vec![ #( #assignments ),* ],
6315 where_clause: #root::core::WhereExpr::Predicate(
6316 #root::core::Filter {
6317 column: #pk_column_lit,
6318 op: #root::core::Op::Eq,
6319 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6320 ::core::clone::Clone::clone(&self.#pk_ident)
6321 ),
6322 }
6323 ),
6324 };
6325 let _ = #root::sql::update_tx(tx, &_query).await?;
6326 ::core::result::Result::Ok(())
6327 }
6328 }
6329 } else {
6330 quote!()
6331 };
6332
6333 let tx_delete_method = {
6334 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6335 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6336 if let Some(pk_ident) = pk_ident_for_tx {
6337 quote! {
6338 /// Delete the row identified by this instance's PK
6339 /// inside an open transaction. Works against any backend
6340 /// that `tx` wraps.
6341 ///
6342 /// # Errors
6343 /// As [`Self::delete_pool`].
6344 pub async fn delete_tx(
6345 &self,
6346 tx: &mut #root::sql::PoolTx<'_>,
6347 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6348 let _query = #root::core::DeleteQuery {
6349 model: <Self as #root::core::Model>::SCHEMA,
6350 where_clause: #root::core::WhereExpr::Predicate(
6351 #root::core::Filter {
6352 column: #pk_column_lit,
6353 op: #root::core::Op::Eq,
6354 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6355 ::core::clone::Clone::clone(&self.#pk_ident)
6356 ),
6357 }
6358 ),
6359 };
6360 #root::sql::delete_tx(tx, &_query).await
6361 }
6362 }
6363 } else {
6364 quote!()
6365 }
6366 };
6367
6368 // Update emission captures both BEFORE and AFTER state — runs an
6369 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6370 // each tracked field's prior value, then after the UPDATE diffs
6371 // against the in-memory `&self`. `diff_changes` drops unchanged
6372 // columns so the JSON only contains the actual delta.
6373 //
6374 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6375 // and binds `_audit_before_pairs`; `audit_update_post` runs
6376 // after the UPDATE and emits the PendingEntry.
6377 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6378 audited_fields
6379 {
6380 if tracked.is_empty() {
6381 (quote!(), quote!())
6382 } else {
6383 let select_cols: String = tracked
6384 .iter()
6385 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6386 .collect::<Vec<_>>()
6387 .join(", ");
6388 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6389 let select_cols_lit = select_cols;
6390 let pk_column_lit_for_select = pk_column_for_select;
6391 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6392 if fields.pk_is_auto {
6393 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6394 } else {
6395 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6396 }
6397 } else {
6398 quote!(0_i64)
6399 };
6400 let before_pairs = tracked.iter().map(|c| {
6401 let column_lit = c.column.as_str();
6402 let value_ty = &c.value_ty;
6403 quote! {
6404 (
6405 #column_lit,
6406 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6407 &_audit_before_row, #column_lit,
6408 ) {
6409 ::core::result::Result::Ok(v) => {
6410 #root::__serde_json::to_value(&v)
6411 .unwrap_or(#root::__serde_json::Value::Null)
6412 }
6413 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6414 },
6415 )
6416 }
6417 });
6418 let after_pairs = tracked.iter().map(|c| {
6419 let column_lit = c.column.as_str();
6420 let ident = &c.ident;
6421 quote! {
6422 (
6423 #column_lit,
6424 #root::__serde_json::to_value(&self.#ident)
6425 .unwrap_or(#root::__serde_json::Value::Null),
6426 )
6427 }
6428 });
6429 let pk_str = audit_pk_to_string.clone();
6430 let pre = quote! {
6431 let _audit_select_sql = ::std::format!(
6432 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6433 #select_cols_lit,
6434 <Self as #root::core::Model>::SCHEMA.table,
6435 #pk_column_lit_for_select,
6436 );
6437 let _audit_before_pairs:
6438 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6439 match #root::sql::sqlx::query(&_audit_select_sql)
6440 .bind(#pk_value_for_bind)
6441 .fetch_optional(&mut *_executor)
6442 .await
6443 {
6444 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6445 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6446 }
6447 _ => ::core::option::Option::None,
6448 };
6449 };
6450 let post = quote! {
6451 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6452 let _audit_after:
6453 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6454 ::std::vec![ #( #after_pairs ),* ];
6455 let _audit_entry = #root::audit::PendingEntry {
6456 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6457 entity_pk: #pk_str,
6458 operation: #root::audit::AuditOp::Update,
6459 source: #root::audit::current_source(),
6460 changes: #root::audit::diff_changes(
6461 &_audit_before,
6462 &_audit_after,
6463 ),
6464 };
6465 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6466 }
6467 };
6468 (pre, post)
6469 }
6470 } else {
6471 (quote!(), quote!())
6472 };
6473
6474 // Bulk-insert audit: capture every row's tracked fields after the
6475 // RETURNING populates each PK, then push one batched INSERT INTO
6476 // audit_log via `emit_many`. One round-trip regardless of N rows.
6477 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6478 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6479 if fields.pk_is_auto {
6480 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6481 } else {
6482 quote!(::std::format!("{}", &_row.#pk_ident))
6483 }
6484 } else {
6485 quote!(::std::string::String::new())
6486 };
6487 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6488 let column_lit = c.column.as_str();
6489 let ident = &c.ident;
6490 quote! {
6491 (
6492 #column_lit,
6493 #root::__serde_json::to_value(&_row.#ident)
6494 .unwrap_or(#root::__serde_json::Value::Null),
6495 )
6496 }
6497 });
6498 quote! {
6499 let _audit_source = #root::audit::current_source();
6500 let mut _audit_entries:
6501 ::std::vec::Vec<#root::audit::PendingEntry> =
6502 ::std::vec::Vec::with_capacity(rows.len());
6503 for _row in rows.iter() {
6504 _audit_entries.push(#root::audit::PendingEntry {
6505 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6506 entity_pk: #row_pk_str,
6507 operation: #root::audit::AuditOp::Create,
6508 source: _audit_source.clone(),
6509 changes: #root::audit::snapshot_changes(&[
6510 #( #row_pairs ),*
6511 ]),
6512 });
6513 }
6514 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6515 }
6516 } else {
6517 quote!()
6518 };
6519
6520 let save_method = if fields.pk_is_auto {
6521 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6522 let pk_column_lit = pk_column.as_str();
6523 let assignments = &fields.update_assignments;
6524 let upsert_cols = &fields.upsert_update_columns;
6525 let upsert_pushes = &fields.insert_pushes;
6526 let upsert_returning = &fields.returning_cols;
6527 let upsert_auto_assigns = &fields.auto_assigns;
6528 // Conflict target: prefer the first declared `unique_together`
6529 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6530 // `BIGSERIAL` and never collide on insert, so a PK-only target
6531 // would silently turn `upsert()` into "always-insert" for
6532 // surrogate-PK models with composite UNIQUE constraints — see
6533 // `RolePermission` / `UserRole` / `UserPermission` in the
6534 // tenancy permission engine. When no `unique_together` is
6535 // declared we keep the PK target (the original behaviour).
6536 let upsert_target_columns: Vec<String> = indexes
6537 .iter()
6538 .find(|i| i.unique && !i.columns.is_empty())
6539 .map(|i| i.columns.clone())
6540 .unwrap_or_else(|| vec![pk_column.clone()]);
6541 let upsert_target_lits = upsert_target_columns
6542 .iter()
6543 .map(String::as_str)
6544 .collect::<Vec<_>>();
6545 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6546 quote!(#root::core::ConflictClause::DoNothing)
6547 } else {
6548 quote!(#root::core::ConflictClause::DoUpdate {
6549 target: ::std::vec![ #( #upsert_target_lits ),* ],
6550 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6551 })
6552 };
6553 Some(quote! {
6554 /// Insert this row if its `Auto<T>` primary key is
6555 /// `Unset`, otherwise update the existing row matching the
6556 /// PK. Mirrors Django's `save()` — caller doesn't need to
6557 /// pick `insert` vs the bulk-update path manually.
6558 ///
6559 /// On the insert branch, populates the PK from `RETURNING`
6560 /// (same behavior as `insert`). On the update branch,
6561 /// writes every non-PK column back; if no row matches the
6562 /// PK, returns `Ok(())` silently.
6563 ///
6564 /// Only generated when the primary key is declared as
6565 /// `Auto<T>`. Models with a manually-managed PK must use
6566 /// `insert` or the QuerySet update builder.
6567 ///
6568 /// # Errors
6569 /// Returns [`#root::sql::ExecError`] for SQL-writing
6570 /// or driver failures.
6571 #[cfg(feature = "postgres")]
6572 pub async fn save(
6573 &mut self,
6574 pool: &#root::sql::sqlx::PgPool,
6575 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6576 #pool_to_save_on
6577 }
6578
6579 /// Like [`Self::save`] but accepts any sqlx executor —
6580 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6581 /// escape hatch for tenant-scoped writes: schema-mode
6582 /// tenants share the registry pool but rely on a per-
6583 /// checkout `SET search_path`, so passing `&PgPool` would
6584 /// silently hit the wrong schema. Acquire a connection
6585 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6586 ///
6587 /// # Errors
6588 /// As [`Self::save`].
6589 #[cfg(feature = "postgres")]
6590 pub async fn save_on #executor_generics (
6591 &mut self,
6592 #executor_param,
6593 ) -> ::core::result::Result<(), #root::sql::ExecError>
6594 #executor_where
6595 {
6596 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6597 return self.insert_on(#executor_passes_to_data_write).await;
6598 }
6599 #audit_update_pre
6600 let _query = #root::core::UpdateQuery {
6601 model: <Self as #root::core::Model>::SCHEMA,
6602 set: ::std::vec![ #( #assignments ),* ],
6603 where_clause: #root::core::WhereExpr::Predicate(
6604 #root::core::Filter {
6605 column: #pk_column_lit,
6606 op: #root::core::Op::Eq,
6607 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6608 ::core::clone::Clone::clone(&self.#pk_ident)
6609 ),
6610 }
6611 ),
6612 };
6613 let _ = #root::sql::__macro_internals::update_on(
6614 #executor_passes_to_data_write,
6615 &_query,
6616 ).await?;
6617 #audit_update_post
6618 ::core::result::Result::Ok(())
6619 }
6620
6621 /// Per-call override for the audit source. Runs
6622 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6623 /// scope so the resulting audit entry records `source`
6624 /// instead of the task-local default. Useful for seed
6625 /// scripts and one-off CLI tools that don't sit inside an
6626 /// admin handler. The override applies only to this call;
6627 /// no global state changes.
6628 ///
6629 /// # Errors
6630 /// As [`Self::save_on`].
6631 #[cfg(feature = "postgres")]
6632 pub async fn save_on_with #executor_generics (
6633 &mut self,
6634 #executor_param,
6635 source: #root::audit::AuditSource,
6636 ) -> ::core::result::Result<(), #root::sql::ExecError>
6637 #executor_where
6638 {
6639 #root::audit::with_source(source, self.save_on(_executor)).await
6640 }
6641
6642 /// Insert this row or update it in-place if the primary key already
6643 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6644 ///
6645 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6646 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6647 /// inserted if absent or all non-PK columns are overwritten if present.
6648 ///
6649 /// # Errors
6650 /// As [`Self::insert_on`].
6651 #[cfg(feature = "postgres")]
6652 pub async fn upsert(
6653 &mut self,
6654 pool: &#root::sql::sqlx::PgPool,
6655 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6656 #pool_to_upsert_on
6657 }
6658
6659 /// Like [`Self::upsert`] but accepts any sqlx executor.
6660 /// See [`Self::save_on`] for tenancy-scoped rationale.
6661 ///
6662 /// # Errors
6663 /// As [`Self::upsert`].
6664 #[cfg(feature = "postgres")]
6665 pub async fn upsert_on #executor_generics (
6666 &mut self,
6667 #executor_param,
6668 ) -> ::core::result::Result<(), #root::sql::ExecError>
6669 #executor_where
6670 {
6671 let mut _columns: ::std::vec::Vec<&'static str> =
6672 ::std::vec::Vec::new();
6673 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6674 ::std::vec::Vec::new();
6675 #( #upsert_pushes )*
6676 let query = #root::core::InsertQuery {
6677 model: <Self as #root::core::Model>::SCHEMA,
6678 columns: _columns,
6679 values: _values,
6680 returning: ::std::vec![ #( #upsert_returning ),* ],
6681 on_conflict: ::core::option::Option::Some(#conflict_clause),
6682 };
6683 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6684 #executor_passes_to_data_write,
6685 &query,
6686 ).await?;
6687 let _returning_row = &_returning_row_v;
6688 #( #upsert_auto_assigns )*
6689 ::core::result::Result::Ok(())
6690 }
6691 })
6692 } else {
6693 None
6694 };
6695
6696 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6697 let pk_column_lit = pk_column.as_str();
6698 // Optional `soft_delete_on` / `restore_on` companions when the
6699 // model has a `#[rustango(soft_delete)]` column. They land
6700 // alongside the regular `delete_on` so callers have both
6701 // options — a hard delete (audit-tracked as a real DELETE) and
6702 // a logical delete (audit-tracked as an UPDATE setting the
6703 // deleted_at column to NOW()).
6704 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6705 let col_lit = col;
6706 let sd_field_ident = fields
6707 .soft_delete_field_ident
6708 .clone()
6709 .expect("soft_delete_column without ident");
6710 quote! {
6711 /// Soft-delete this row by setting its
6712 /// `#[rustango(soft_delete)]` column to `NOW()`.
6713 /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6714 /// the row stays in the table; query helpers can
6715 /// filter it out by checking the column for `IS NOT
6716 /// NULL`.
6717 ///
6718 /// # Errors
6719 /// As [`Self::delete`].
6720 pub async fn soft_delete_on #executor_generics (
6721 &self,
6722 #executor_param,
6723 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6724 #executor_where
6725 {
6726 let _query = #root::core::UpdateQuery {
6727 model: <Self as #root::core::Model>::SCHEMA,
6728 set: ::std::vec![
6729 #root::core::Assignment {
6730 column: #col_lit,
6731 value: ::core::convert::Into::<#root::core::Expr>::into(
6732 ::core::convert::Into::<#root::core::SqlValue>::into(
6733 #root::__chrono::Utc::now()
6734 )
6735 ),
6736 },
6737 ],
6738 where_clause: #root::core::WhereExpr::Predicate(
6739 #root::core::Filter {
6740 column: #pk_column_lit,
6741 op: #root::core::Op::Eq,
6742 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6743 ::core::clone::Clone::clone(&self.#pk_ident)
6744 ),
6745 }
6746 ),
6747 };
6748 let _affected = #root::sql::__macro_internals::update_on(
6749 #executor_passes_to_data_write,
6750 &_query,
6751 ).await?;
6752 #audit_softdelete_emit
6753 ::core::result::Result::Ok(_affected)
6754 }
6755
6756 /// Inverse of [`Self::soft_delete_on`] — clears the
6757 /// soft-delete column back to NULL so the row is
6758 /// considered live again.
6759 ///
6760 /// # Errors
6761 /// As [`Self::delete`].
6762 pub async fn restore_on #executor_generics (
6763 &self,
6764 #executor_param,
6765 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6766 #executor_where
6767 {
6768 let _query = #root::core::UpdateQuery {
6769 model: <Self as #root::core::Model>::SCHEMA,
6770 set: ::std::vec![
6771 #root::core::Assignment {
6772 column: #col_lit,
6773 value: ::core::convert::Into::<#root::core::Expr>::into(
6774 #root::core::SqlValue::Null
6775 ),
6776 },
6777 ],
6778 where_clause: #root::core::WhereExpr::Predicate(
6779 #root::core::Filter {
6780 column: #pk_column_lit,
6781 op: #root::core::Op::Eq,
6782 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6783 ::core::clone::Clone::clone(&self.#pk_ident)
6784 ),
6785 }
6786 ),
6787 };
6788 let _affected = #root::sql::__macro_internals::update_on(
6789 #executor_passes_to_data_write,
6790 &_query,
6791 ).await?;
6792 #audit_restore_emit
6793 ::core::result::Result::Ok(_affected)
6794 }
6795
6796 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6797 /// — takes [`#root::sql::Pool`] and dispatches per
6798 /// backend. Eloquent `Model::delete()` semantics on
6799 /// soft-delete-enabled models (closes #821 partial).
6800 ///
6801 /// Sets the `#[rustango(soft_delete)]` column to
6802 /// `NOW()` on every backend. Query helpers
6803 /// (`QuerySet::active()` / `only_trashed()`,
6804 /// `soft_delete::active_filter` /
6805 /// `compose_with_active`) filter trashed rows out by
6806 /// reading `IS NULL` on the same column.
6807 ///
6808 /// # Errors
6809 /// As [`#root::sql::update_pool`].
6810 pub async fn soft_delete(
6811 &self,
6812 pool: &#root::sql::Pool,
6813 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6814 let _query = #root::core::UpdateQuery {
6815 model: <Self as #root::core::Model>::SCHEMA,
6816 set: ::std::vec![
6817 #root::core::Assignment {
6818 column: #col_lit,
6819 value: ::core::convert::Into::<#root::core::Expr>::into(
6820 ::core::convert::Into::<#root::core::SqlValue>::into(
6821 #root::__chrono::Utc::now()
6822 )
6823 ),
6824 },
6825 ],
6826 where_clause: #root::core::WhereExpr::Predicate(
6827 #root::core::Filter {
6828 column: #pk_column_lit,
6829 op: #root::core::Op::Eq,
6830 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6831 ::core::clone::Clone::clone(&self.#pk_ident)
6832 ),
6833 }
6834 ),
6835 };
6836 #root::sql::update_pool(pool, &_query).await
6837 }
6838
6839 /// Tri-dialect counterpart of [`Self::restore_on`].
6840 /// Clears the `#[rustango(soft_delete)]` column back
6841 /// to `NULL`, marking the row live again. Eloquent
6842 /// `Model::restore()` parity.
6843 ///
6844 /// # Errors
6845 /// As [`#root::sql::update_pool`].
6846 pub async fn restore(
6847 &self,
6848 pool: &#root::sql::Pool,
6849 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6850 let _query = #root::core::UpdateQuery {
6851 model: <Self as #root::core::Model>::SCHEMA,
6852 set: ::std::vec![
6853 #root::core::Assignment {
6854 column: #col_lit,
6855 value: ::core::convert::Into::<#root::core::Expr>::into(
6856 #root::core::SqlValue::Null
6857 ),
6858 },
6859 ],
6860 where_clause: #root::core::WhereExpr::Predicate(
6861 #root::core::Filter {
6862 column: #pk_column_lit,
6863 op: #root::core::Op::Eq,
6864 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6865 ::core::clone::Clone::clone(&self.#pk_ident)
6866 ),
6867 }
6868 ),
6869 };
6870 #root::sql::update_pool(pool, &_query).await
6871 }
6872
6873 /// Hard-delete this row, ignoring the soft-delete
6874 /// column. Eloquent `Model::forceDelete()` parity —
6875 /// the escape hatch when you need to actually purge
6876 /// data (GDPR, fixture cleanup, etc.).
6877 ///
6878 /// Equivalent to [`Self::delete_pool`] (the framework's
6879 /// non-soft delete) — exposed under the Eloquent name
6880 /// for muscle-memory + so soft-delete-enabled models
6881 /// have all three operations (soft / restore / force)
6882 /// in one place.
6883 ///
6884 /// # Errors
6885 /// As [`Self::delete_pool`].
6886 pub async fn force_delete(
6887 &self,
6888 pool: &#root::sql::Pool,
6889 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6890 Self::delete_pool(self, pool).await
6891 }
6892
6893 /// Returns `true` when this row is soft-deleted (its
6894 /// `#[rustango(soft_delete)]` column is currently
6895 /// set — Eloquent `$model->trashed()` parity).
6896 ///
6897 /// Pure in-memory predicate over `&self`; does not
6898 /// hit the database. Useful in admin/template code
6899 /// like `{% if post.trashed() %}…{% endif %}` and in
6900 /// guard clauses on restore/force-delete flows.
6901 pub fn trashed(&self) -> bool {
6902 ::core::option::Option::is_some(&self.#sd_field_ident)
6903 }
6904
6905 /// Fetch every row whose `#[rustango(soft_delete)]`
6906 /// column is `NULL` (a.k.a. the live, non-trashed
6907 /// rows). Eloquent's default `Model::all()` behavior on
6908 /// a soft-delete model (Eloquent auto-scopes trashed
6909 /// rows out; rustango doesn't have auto-scoping yet —
6910 /// see issue #820 — so this is the explicit shortcut).
6911 ///
6912 /// One-liner over `QuerySet::<Self>::default()
6913 /// .active().fetch_pool(pool)`. Closes #821 partial.
6914 ///
6915 /// # Errors
6916 /// As [`#root::sql::FetcherPool::fetch_pool`].
6917 pub async fn active(
6918 pool: &#root::sql::Pool,
6919 ) -> ::core::result::Result<
6920 ::std::vec::Vec<Self>,
6921 #root::sql::ExecError,
6922 > {
6923 use #root::sql::FetcherPool as _;
6924 #root::query::QuerySet::<Self>::default()
6925 .active()
6926 .fetch_pool(pool)
6927 .await
6928 }
6929
6930 /// Fetch ONLY soft-deleted rows. Eloquent
6931 /// `Model::onlyTrashed()->get()` parity — drives the
6932 /// admin "Trash" page, restore flows, GDPR purge
6933 /// scans, etc.
6934 ///
6935 /// One-liner over `QuerySet::<Self>::default()
6936 /// .only_trashed().fetch_pool(pool)`. Closes #821
6937 /// partial.
6938 ///
6939 /// # Errors
6940 /// As [`#root::sql::FetcherPool::fetch_pool`].
6941 pub async fn only_trashed(
6942 pool: &#root::sql::Pool,
6943 ) -> ::core::result::Result<
6944 ::std::vec::Vec<Self>,
6945 #root::sql::ExecError,
6946 > {
6947 use #root::sql::FetcherPool as _;
6948 #root::query::QuerySet::<Self>::default()
6949 .only_trashed()
6950 .fetch_pool(pool)
6951 .await
6952 }
6953
6954 /// Fetch every row, both live and soft-deleted.
6955 /// Eloquent `Model::withTrashed()->get()` parity.
6956 ///
6957 /// Today every queryset already includes trashed rows
6958 /// (rustango has no global-scope tracking yet — issue
6959 /// #820), so this is functionally equivalent to
6960 /// [`Self::all_pool`]. Exposed as a named shortcut so
6961 /// soft-delete-aware code reads `Model::with_trashed_pool`
6962 /// rather than `Model::all_pool` — keeps intent visible
6963 /// in callers and stays correct when auto-scoping lands.
6964 ///
6965 /// Closes #821 partial.
6966 ///
6967 /// # Errors
6968 /// As [`#root::sql::FetcherPool::fetch_pool`].
6969 pub async fn with_trashed(
6970 pool: &#root::sql::Pool,
6971 ) -> ::core::result::Result<
6972 ::std::vec::Vec<Self>,
6973 #root::sql::ExecError,
6974 > {
6975 use #root::sql::FetcherPool as _;
6976 #root::query::QuerySet::<Self>::default()
6977 .with_trashed()
6978 .fetch_pool(pool)
6979 .await
6980 }
6981
6982 }
6983 } else {
6984 quote!()
6985 };
6986 quote! {
6987 /// Delete the row identified by this instance's primary key.
6988 ///
6989 /// Returns the number of rows affected (0 or 1).
6990 ///
6991 /// # Errors
6992 /// Returns [`#root::sql::ExecError`] for SQL-writing or
6993 /// driver failures.
6994 #[cfg(feature = "postgres")]
6995 pub async fn delete(
6996 &self,
6997 pool: &#root::sql::sqlx::PgPool,
6998 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6999 #pool_to_delete_on
7000 }
7001
7002 /// Like [`Self::delete`] but accepts any sqlx executor —
7003 /// for tenant-scoped deletes against an explicitly-acquired
7004 /// connection. See [`Self::save_on`] for the rationale.
7005 ///
7006 /// # Errors
7007 /// As [`Self::delete`].
7008 #[cfg(feature = "postgres")]
7009 pub async fn delete_on #executor_generics (
7010 &self,
7011 #executor_param,
7012 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7013 #executor_where
7014 {
7015 let query = #root::core::DeleteQuery {
7016 model: <Self as #root::core::Model>::SCHEMA,
7017 where_clause: #root::core::WhereExpr::Predicate(
7018 #root::core::Filter {
7019 column: #pk_column_lit,
7020 op: #root::core::Op::Eq,
7021 value: ::core::convert::Into::<#root::core::SqlValue>::into(
7022 ::core::clone::Clone::clone(&self.#pk_ident)
7023 ),
7024 }
7025 ),
7026 };
7027 let _affected = #root::sql::__macro_internals::delete_on(
7028 #executor_passes_to_data_write,
7029 &query,
7030 ).await?;
7031 #audit_delete_emit
7032 ::core::result::Result::Ok(_affected)
7033 }
7034
7035 /// Per-call audit-source override for [`Self::delete_on`].
7036 /// See [`Self::save_on_with`] for shape rationale.
7037 ///
7038 /// # Errors
7039 /// As [`Self::delete_on`].
7040 #[cfg(feature = "postgres")]
7041 pub async fn delete_on_with #executor_generics (
7042 &self,
7043 #executor_param,
7044 source: #root::audit::AuditSource,
7045 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7046 #executor_where
7047 {
7048 #root::audit::with_source(source, self.delete_on(_executor)).await
7049 }
7050 #pool_delete_method
7051 #pool_insert_method
7052 #pool_save_method
7053 #refresh_replicate_methods
7054 #tx_delete_method
7055 #tx_insert_method
7056 #tx_save_method
7057 #soft_delete_methods
7058
7059 /// Returns `true` when `other` represents the same DB
7060 /// row as `self` — i.e. their primary keys compare
7061 /// equal. Eloquent `$model->is($other)` parity.
7062 ///
7063 /// Because both arguments are typed `&Self`, the
7064 /// model/table check is automatic — `Post::is` cannot
7065 /// be invoked against a `Comment` at compile time. Only
7066 /// the PK has to be compared at runtime.
7067 pub fn is(&self, other: &Self) -> bool {
7068 self.#pk_ident == other.#pk_ident
7069 }
7070
7071 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7072 /// parity.
7073 pub fn is_not(&self, other: &Self) -> bool {
7074 self.#pk_ident != other.#pk_ident
7075 }
7076
7077 /// Returns this row's primary-key value as an
7078 /// [`#root::core::SqlValue`]. Eloquent
7079 /// `$model->getKey()` parity.
7080 ///
7081 /// Useful when you need to thread the PK through a
7082 /// generic `Into<SqlValue>`-bound API without knowing the
7083 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7084 #[must_use]
7085 pub fn get_key(&self) -> #root::core::SqlValue {
7086 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7087 }
7088
7089 }
7090 });
7091
7092 let insert_method = if fields.has_auto {
7093 let pushes = &fields.insert_pushes;
7094 let returning_cols = &fields.returning_cols;
7095 let auto_assigns = &fields.auto_assigns;
7096 quote! {
7097 /// Insert this row into its table. Skips columns whose
7098 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7099 /// sequence fills them in, then reads each `Auto` column
7100 /// back via `RETURNING` and stores it on `self`.
7101 ///
7102 /// # Errors
7103 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7104 /// driver failures.
7105 #[cfg(feature = "postgres")]
7106 pub async fn insert(
7107 &mut self,
7108 pool: &#root::sql::sqlx::PgPool,
7109 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7110 #pool_to_insert_on
7111 }
7112
7113 /// Like [`Self::insert`] but accepts any sqlx executor.
7114 /// See [`Self::save_on`] for tenancy-scoped rationale.
7115 ///
7116 /// # Errors
7117 /// As [`Self::insert`].
7118 #[cfg(feature = "postgres")]
7119 pub async fn insert_on #executor_generics (
7120 &mut self,
7121 #executor_param,
7122 ) -> ::core::result::Result<(), #root::sql::ExecError>
7123 #executor_where
7124 {
7125 let mut _columns: ::std::vec::Vec<&'static str> =
7126 ::std::vec::Vec::new();
7127 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7128 ::std::vec::Vec::new();
7129 #( #pushes )*
7130 let query = #root::core::InsertQuery {
7131 model: <Self as #root::core::Model>::SCHEMA,
7132 columns: _columns,
7133 values: _values,
7134 returning: ::std::vec![ #( #returning_cols ),* ],
7135 on_conflict: ::core::option::Option::None,
7136 };
7137 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7138 #executor_passes_to_data_write,
7139 &query,
7140 ).await?;
7141 let _returning_row = &_returning_row_v;
7142 #( #auto_assigns )*
7143 #audit_insert_emit
7144 ::core::result::Result::Ok(())
7145 }
7146
7147 /// Per-call audit-source override for [`Self::insert_on`].
7148 /// See [`Self::save_on_with`] for shape rationale.
7149 ///
7150 /// # Errors
7151 /// As [`Self::insert_on`].
7152 #[cfg(feature = "postgres")]
7153 pub async fn insert_on_with #executor_generics (
7154 &mut self,
7155 #executor_param,
7156 source: #root::audit::AuditSource,
7157 ) -> ::core::result::Result<(), #root::sql::ExecError>
7158 #executor_where
7159 {
7160 #root::audit::with_source(source, self.insert_on(_executor)).await
7161 }
7162 }
7163 } else {
7164 let insert_columns = &fields.insert_columns;
7165 let insert_values = &fields.insert_values;
7166 quote! {
7167 /// Insert this row into its table.
7168 ///
7169 /// # Errors
7170 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7171 /// driver failures.
7172 #[cfg(feature = "postgres")]
7173 pub async fn insert(
7174 &self,
7175 pool: &#root::sql::sqlx::PgPool,
7176 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7177 self.insert_on(pool).await
7178 }
7179
7180 /// Like [`Self::insert`] but accepts any sqlx executor.
7181 /// See [`Self::save_on`] for tenancy-scoped rationale.
7182 ///
7183 /// # Errors
7184 /// As [`Self::insert`].
7185 #[cfg(feature = "postgres")]
7186 pub async fn insert_on<'_c, _E>(
7187 &self,
7188 _executor: _E,
7189 ) -> ::core::result::Result<(), #root::sql::ExecError>
7190 where
7191 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7192 {
7193 let query = #root::core::InsertQuery {
7194 model: <Self as #root::core::Model>::SCHEMA,
7195 columns: ::std::vec![ #( #insert_columns ),* ],
7196 values: ::std::vec![ #( #insert_values ),* ],
7197 returning: ::std::vec::Vec::new(),
7198 on_conflict: ::core::option::Option::None,
7199 };
7200 #root::sql::__macro_internals::insert_on(_executor, &query).await
7201 }
7202 }
7203 };
7204
7205 let bulk_insert_method = if fields.has_auto {
7206 let cols_no_auto = &fields.bulk_columns_no_auto;
7207 let cols_all = &fields.bulk_columns_all;
7208 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7209 let pushes_all = &fields.bulk_pushes_all;
7210 let returning_cols = &fields.returning_cols;
7211 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7212 let uniformity = &fields.bulk_auto_uniformity;
7213 let first_auto_ident = fields
7214 .first_auto_ident
7215 .as_ref()
7216 .expect("has_auto implies first_auto_ident is Some");
7217 quote! {
7218 /// Bulk-insert `rows` in a single round-trip. Every row's
7219 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7220 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7221 /// (caller-supplied values). Mixed Set/Unset is rejected
7222 /// — call `insert` per row for that case.
7223 ///
7224 /// Empty slice is a no-op. Each row's `Auto` fields are
7225 /// populated from the `RETURNING` clause in input order
7226 /// before this returns.
7227 ///
7228 /// # Errors
7229 /// Returns [`#root::sql::ExecError`] for validation,
7230 /// SQL-writing, mixed-Auto rejection, or driver failures.
7231 #[cfg(feature = "postgres")]
7232 pub async fn bulk_insert(
7233 rows: &mut [Self],
7234 pool: &#root::sql::sqlx::PgPool,
7235 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7236 #pool_to_bulk_insert_on
7237 }
7238
7239 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7240 /// See [`Self::save_on`] for tenancy-scoped rationale.
7241 ///
7242 /// # Errors
7243 /// As [`Self::bulk_insert`].
7244 #[cfg(feature = "postgres")]
7245 pub async fn bulk_insert_on #executor_generics (
7246 rows: &mut [Self],
7247 #executor_param,
7248 ) -> ::core::result::Result<(), #root::sql::ExecError>
7249 #executor_where
7250 {
7251 if rows.is_empty() {
7252 return ::core::result::Result::Ok(());
7253 }
7254 let _first_unset = matches!(
7255 rows[0].#first_auto_ident,
7256 #root::sql::Auto::Unset
7257 );
7258 #( #uniformity )*
7259
7260 let mut _all_rows: ::std::vec::Vec<
7261 ::std::vec::Vec<#root::core::SqlValue>,
7262 > = ::std::vec::Vec::with_capacity(rows.len());
7263 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7264 for _row in rows.iter() {
7265 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7266 ::std::vec::Vec::new();
7267 #( #pushes_no_auto )*
7268 _all_rows.push(_row_vals);
7269 }
7270 ::std::vec![ #( #cols_no_auto ),* ]
7271 } else {
7272 for _row in rows.iter() {
7273 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7274 ::std::vec::Vec::new();
7275 #( #pushes_all )*
7276 _all_rows.push(_row_vals);
7277 }
7278 ::std::vec![ #( #cols_all ),* ]
7279 };
7280
7281 let _query = #root::core::BulkInsertQuery {
7282 model: <Self as #root::core::Model>::SCHEMA,
7283 columns: _columns,
7284 rows: _all_rows,
7285 returning: ::std::vec![ #( #returning_cols ),* ],
7286 on_conflict: ::core::option::Option::None,
7287 };
7288 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7289 #executor_passes_to_data_write,
7290 &_query,
7291 ).await?;
7292 if _returned.len() != rows.len() {
7293 return ::core::result::Result::Err(
7294 #root::sql::ExecError::Sql(
7295 #root::sql::SqlError::BulkInsertReturningMismatch {
7296 expected: rows.len(),
7297 actual: _returned.len(),
7298 }
7299 )
7300 );
7301 }
7302 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7303 #auto_assigns_for_row
7304 }
7305 #audit_bulk_insert_emit
7306 ::core::result::Result::Ok(())
7307 }
7308 }
7309 } else {
7310 let cols_all = &fields.bulk_columns_all;
7311 let pushes_all = &fields.bulk_pushes_all;
7312 quote! {
7313 /// Bulk-insert `rows` in a single round-trip. Every row's
7314 /// fields are written verbatim — there are no `Auto<T>`
7315 /// fields on this model.
7316 ///
7317 /// Empty slice is a no-op.
7318 ///
7319 /// # Errors
7320 /// Returns [`#root::sql::ExecError`] for validation,
7321 /// SQL-writing, or driver failures.
7322 #[cfg(feature = "postgres")]
7323 pub async fn bulk_insert(
7324 rows: &[Self],
7325 pool: &#root::sql::sqlx::PgPool,
7326 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7327 Self::bulk_insert_on(rows, pool).await
7328 }
7329
7330 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7331 /// See [`Self::save_on`] for tenancy-scoped rationale.
7332 ///
7333 /// # Errors
7334 /// As [`Self::bulk_insert`].
7335 #[cfg(feature = "postgres")]
7336 pub async fn bulk_insert_on<'_c, _E>(
7337 rows: &[Self],
7338 _executor: _E,
7339 ) -> ::core::result::Result<(), #root::sql::ExecError>
7340 where
7341 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7342 {
7343 if rows.is_empty() {
7344 return ::core::result::Result::Ok(());
7345 }
7346 let mut _all_rows: ::std::vec::Vec<
7347 ::std::vec::Vec<#root::core::SqlValue>,
7348 > = ::std::vec::Vec::with_capacity(rows.len());
7349 for _row in rows.iter() {
7350 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7351 ::std::vec::Vec::new();
7352 #( #pushes_all )*
7353 _all_rows.push(_row_vals);
7354 }
7355 let _query = #root::core::BulkInsertQuery {
7356 model: <Self as #root::core::Model>::SCHEMA,
7357 columns: ::std::vec![ #( #cols_all ),* ],
7358 rows: _all_rows,
7359 returning: ::std::vec::Vec::new(),
7360 on_conflict: ::core::option::Option::None,
7361 };
7362 let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7363 ::core::result::Result::Ok(())
7364 }
7365 }
7366 };
7367
7368 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7369 // (no postgres-feature gate); routes through the existing
7370 // `bulk_insert_pool` + per-dialect conflict writer.
7371 //
7372 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7373 // sequence picks the PK for fresh inserts; the UPDATE branch never
7374 // touches the Auto column.
7375 let bulk_upsert_pool_method = {
7376 // Pick the "no Auto" columns when the model has Auto fields,
7377 // else every column.
7378 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7379 (
7380 fields.bulk_columns_no_auto.clone(),
7381 fields.bulk_pushes_no_auto.clone(),
7382 )
7383 } else {
7384 (
7385 fields.bulk_columns_all.clone(),
7386 fields.bulk_pushes_all.clone(),
7387 )
7388 };
7389 quote! {
7390 /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7391 /// canonical "import a batch idempotently" shape. Issue #267
7392 /// / T1.5.
7393 ///
7394 /// Per-row values are extracted and lowered into a
7395 /// [`#root::core::BulkInsertQuery`] with
7396 /// `on_conflict = DoUpdate { target, update_columns }`. The
7397 /// writer dispatches per-dialect:
7398 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7399 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7400 ///
7401 /// `target` names the column(s) whose unique constraint
7402 /// defines the conflict (typically a `unique` or
7403 /// `unique_together` natural-key column, NOT the `Auto<T>`
7404 /// PK). `update_cols` names the columns to overwrite on
7405 /// conflict — every other column is left untouched on the
7406 /// existing row.
7407 ///
7408 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7409 /// picks the PK on insert; the update path never touches
7410 /// the Auto column). Auto-set rows trigger a hard error.
7411 /// Empty slice is a no-op.
7412 ///
7413 /// # Errors
7414 /// Returns [`#root::sql::ExecError`] for validation,
7415 /// SQL-writing, or driver failures.
7416 pub async fn bulk_upsert_pool(
7417 rows: &[Self],
7418 target: &[&'static str],
7419 update_cols: &[&'static str],
7420 pool: &#root::sql::Pool,
7421 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7422 if rows.is_empty() {
7423 return ::core::result::Result::Ok(());
7424 }
7425 let mut _all_rows: ::std::vec::Vec<
7426 ::std::vec::Vec<#root::core::SqlValue>,
7427 > = ::std::vec::Vec::with_capacity(rows.len());
7428 for _row in rows.iter() {
7429 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7430 ::std::vec::Vec::new();
7431 #( #upsert_pushes )*
7432 _all_rows.push(_row_vals);
7433 }
7434 let _query = #root::core::BulkInsertQuery {
7435 model: <Self as #root::core::Model>::SCHEMA,
7436 columns: ::std::vec![ #( #upsert_cols ),* ],
7437 rows: _all_rows,
7438 returning: ::std::vec::Vec::new(),
7439 on_conflict: ::core::option::Option::Some(
7440 #root::core::ConflictClause::DoUpdate {
7441 target: target.to_vec(),
7442 update_columns: update_cols.to_vec(),
7443 }
7444 ),
7445 };
7446 #root::sql::bulk_insert_pool(pool, &_query).await
7447 }
7448
7449 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7450 /// skip rows that would violate a unique constraint. Issue
7451 /// #267 / T1.5. Same per-dialect dispatch as
7452 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7453 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7454 /// <pivot> = <pivot>` (MySQL no-op write).
7455 ///
7456 /// # Errors
7457 /// As [`Self::bulk_upsert_pool`].
7458 pub async fn bulk_insert_or_ignore_pool(
7459 rows: &[Self],
7460 pool: &#root::sql::Pool,
7461 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7462 if rows.is_empty() {
7463 return ::core::result::Result::Ok(());
7464 }
7465 let mut _all_rows: ::std::vec::Vec<
7466 ::std::vec::Vec<#root::core::SqlValue>,
7467 > = ::std::vec::Vec::with_capacity(rows.len());
7468 for _row in rows.iter() {
7469 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7470 ::std::vec::Vec::new();
7471 #( #upsert_pushes )*
7472 _all_rows.push(_row_vals);
7473 }
7474 let _query = #root::core::BulkInsertQuery {
7475 model: <Self as #root::core::Model>::SCHEMA,
7476 columns: ::std::vec![ #( #upsert_cols ),* ],
7477 rows: _all_rows,
7478 returning: ::std::vec::Vec::new(),
7479 on_conflict: ::core::option::Option::Some(
7480 #root::core::ConflictClause::DoNothing
7481 ),
7482 };
7483 #root::sql::bulk_insert_pool(pool, &_query).await
7484 }
7485 }
7486 };
7487
7488 // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7489 // `QuerySet.bulk_update`. The SQL/IR/executor stack
7490 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7491 // `write_bulk_update_*` writers) already existed; what was missing
7492 // was the per-model constructor that maps `&[Self]` + a runtime
7493 // column list into rows of `[pk, col_vals…]` so callers don't
7494 // hand-build the IR. Emitted only when the model has a primary key
7495 // (the PK is the join key and can't itself be updated).
7496 let bulk_update_method = match &fields.primary_key {
7497 None => quote! {},
7498 Some((pk_ident, pk_col)) => {
7499 // One pair of match arms per non-PK column: a validation arm
7500 // resolving the runtime name to its `&'static str` column,
7501 // and a value arm pushing that field off the row.
7502 let mut col_arms: Vec<TokenStream2> = Vec::new();
7503 let mut val_arms: Vec<TokenStream2> = Vec::new();
7504 for entry in &fields.column_entries {
7505 if &entry.column == pk_col {
7506 continue;
7507 }
7508 let col = &entry.column;
7509 let ident = &entry.ident;
7510 col_arms.push(quote! { #col => #col, });
7511 val_arms.push(quote! {
7512 #col => _row_vals.push(
7513 ::core::convert::Into::<#root::core::SqlValue>::into(
7514 ::core::clone::Clone::clone(&_o.#ident)
7515 )
7516 ),
7517 });
7518 }
7519 quote! {
7520 /// Django's `QuerySet.bulk_update(objs, fields)` — write
7521 /// per-row-different values for the named `fields` across
7522 /// every object in `objs` in a single statement, matched
7523 /// by primary key.
7524 ///
7525 /// `fields` names the **columns** to update. The primary
7526 /// key identifies each row and cannot itself be updated
7527 /// (pass it and you get
7528 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7529 /// Empty `objs` or `fields` is a no-op returning `0`.
7530 /// Objects whose PK matches no row are simply not updated.
7531 /// Returns the number of rows affected.
7532 ///
7533 /// Tri-dialect: lowers to one
7534 /// [`#root::core::BulkUpdateQuery`] and dispatches
7535 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7536 /// a CTE + correlated subquery on SQLite, an inner
7537 /// `JOIN (VALUES …)` on MySQL.
7538 ///
7539 /// # Errors
7540 /// [`#root::core::QueryError::UnknownField`] for a name
7541 /// that isn't a column on this model,
7542 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7543 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7544 /// SQL-writing / driver failures.
7545 pub async fn bulk_update(
7546 objs: &[Self],
7547 fields: &[&str],
7548 pool: &#root::sql::Pool,
7549 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7550 if objs.is_empty() || fields.is_empty() {
7551 return ::core::result::Result::Ok(0);
7552 }
7553 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7554 let mut _update_columns: ::std::vec::Vec<&'static str> =
7555 ::std::vec::Vec::with_capacity(fields.len());
7556 for &_f in fields {
7557 let _col: &'static str = match _f {
7558 #pk_col => {
7559 return ::core::result::Result::Err(
7560 ::core::convert::Into::into(
7561 #root::core::QueryError::BulkUpdatePrimaryKey {
7562 model: _model_name,
7563 field: ::std::string::ToString::to_string(_f),
7564 }
7565 )
7566 );
7567 }
7568 #( #col_arms )*
7569 _ => {
7570 return ::core::result::Result::Err(
7571 ::core::convert::Into::into(
7572 #root::core::QueryError::UnknownField {
7573 model: _model_name,
7574 field: ::std::string::ToString::to_string(_f),
7575 }
7576 )
7577 );
7578 }
7579 };
7580 _update_columns.push(_col);
7581 }
7582 let mut _rows: ::std::vec::Vec<
7583 ::std::vec::Vec<#root::core::SqlValue>,
7584 > = ::std::vec::Vec::with_capacity(objs.len());
7585 for _o in objs.iter() {
7586 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7587 ::std::vec::Vec::with_capacity(fields.len() + 1);
7588 // PK first — the writers expect `[pk, …update cols]`.
7589 _row_vals.push(
7590 ::core::convert::Into::<#root::core::SqlValue>::into(
7591 ::core::clone::Clone::clone(&_o.#pk_ident)
7592 )
7593 );
7594 for &_f in fields {
7595 match _f {
7596 #( #val_arms )*
7597 // Unreachable: every name was validated
7598 // against the same arm set above.
7599 _ => {}
7600 }
7601 }
7602 _rows.push(_row_vals);
7603 }
7604 let _query = #root::core::BulkUpdateQuery {
7605 model: <Self as #root::core::Model>::SCHEMA,
7606 update_columns: _update_columns,
7607 rows: _rows,
7608 };
7609 #root::sql::bulk_update_pool(pool, &_query).await
7610 }
7611 }
7612 }
7613 };
7614
7615 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7616 quote! {
7617 /// Hidden runtime accessor for the primary-key value as a
7618 /// [`SqlValue`]. Used by reverse-relation helpers
7619 /// (`<parent>::<child>_set`) emitted from sibling models'
7620 /// FK fields. Not part of the public API.
7621 #[doc(hidden)]
7622 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7623 ::core::convert::Into::<#root::core::SqlValue>::into(
7624 ::core::clone::Clone::clone(&self.#pk_ident)
7625 )
7626 }
7627 }
7628 });
7629
7630 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7631 quote! {
7632 impl #root::sql::HasPkValue for #struct_name {
7633 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7634 ::core::convert::Into::<#root::core::SqlValue>::into(
7635 ::core::clone::Clone::clone(&self.#pk_ident)
7636 )
7637 }
7638 }
7639 }
7640 });
7641
7642 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7643
7644 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7645 // dispatch to the right per-backend body without the macro emitting
7646 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7647 // so audited models with non-Auto PKs (which still go through
7648 // `insert_one_with_audit` → `apply_auto_pk`) link.
7649 let assign_auto_pk_pool_impl = {
7650 let auto_assigns = &fields.auto_assigns;
7651 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7652 // so the body is structurally identical to `auto_assigns` —
7653 // only the helper name swaps from `try_get_returning` to
7654 // `try_get_returning_sqlite` so the closure typechecks against
7655 // a `SqliteRow` instead of a `PgRow`.
7656 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7657 .auto_field_idents
7658 .iter()
7659 .map(|(ident, column)| {
7660 quote! {
7661 self.#ident = #root::sql::try_get_returning_sqlite(
7662 _returning_row, #column
7663 )?;
7664 }
7665 })
7666 .collect();
7667 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7668 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7669 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7670 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7671 // those models can't use AUTO_INCREMENT). The trait is only
7672 // touched on the MySQL arm at runtime, so PG-only consumers
7673 // never see the bound failure.
7674 //
7675 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7676 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7677 // runtime with "multi-column RETURNING". MySQL has no
7678 // multi-column RETURNING semantic and a follow-up SELECT
7679 // would need cross-trait plumbing. Pragmatic shape: succeed
7680 // with the FIRST Auto field populated from LAST_INSERT_ID();
7681 // any other Auto fields stay `Auto::Unset`. Callers that
7682 // need the DB-defaulted timestamp / UUID can re-fetch the
7683 // row by PK after `save_pool`. Fixes the cookbook chapter
7684 // 12 dialect divergence.
7685 let value_ty = fields
7686 .first_auto_value_ty
7687 .as_ref()
7688 .expect("first_auto_value_ty set whenever first_auto_ident is");
7689 quote! {
7690 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7691 ::rustango_from_mysql_auto_id(_id)?;
7692 self.#first = #root::sql::Auto::Set(_converted);
7693 ::core::result::Result::Ok(())
7694 }
7695 } else {
7696 quote! {
7697 let _ = _id;
7698 ::core::result::Result::Ok(())
7699 }
7700 };
7701 quote! {
7702 impl #root::sql::AssignAutoPkPool for #struct_name {
7703 fn __rustango_assign_from_pg_row(
7704 &mut self,
7705 _returning_row: &#root::sql::PgReturningRow,
7706 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7707 #( #auto_assigns )*
7708 ::core::result::Result::Ok(())
7709 }
7710 fn __rustango_assign_from_mysql_id(
7711 &mut self,
7712 _id: i64,
7713 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7714 #mysql_body
7715 }
7716 fn __rustango_assign_from_sqlite_row(
7717 &mut self,
7718 _returning_row: &#root::sql::SqliteReturningRow,
7719 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7720 #( #auto_assigns_sqlite )*
7721 ::core::result::Result::Ok(())
7722 }
7723 }
7724 }
7725 };
7726
7727 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7728 let aliased_row_helper = quote! {
7729 /// Decode a row's aliased target columns (produced by
7730 /// `select_related`'s LEFT JOIN) into a fresh instance of
7731 /// this model. Reads each column via
7732 /// `format!("{prefix}__{col}")`, matching the alias the
7733 /// SELECT writer emitted. Slice 9.0d.
7734 #[doc(hidden)]
7735 #[cfg(feature = "postgres")]
7736 pub fn __rustango_from_aliased_row(
7737 row: &#root::sql::sqlx::postgres::PgRow,
7738 prefix: &str,
7739 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7740 ::core::result::Result::Ok(Self {
7741 #( #from_aliased_row_inits ),*
7742 })
7743 }
7744 };
7745 // v0.23.0-batch8 — MySQL counterpart, gated through the
7746 // cfg-aware macro_rules so PG-only builds expand to nothing.
7747 let aliased_row_helper_my = quote! {
7748 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7749 #( #from_aliased_row_inits ),*
7750 });
7751 };
7752
7753 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7754 // pattern + cfg gate on the `sqlite` feature.
7755 let aliased_row_helper_sqlite = quote! {
7756 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7757 #( #from_aliased_row_inits ),*
7758 });
7759 };
7760
7761 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7762 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7763 let load_related_impl_sqlite =
7764 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7765
7766 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7767 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7768 // default `Self::objects()`. Each accessor returns a fresh
7769 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7770 // methods the user defined.
7771 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7772 .iter()
7773 .map(|fn_ident| {
7774 let model_name_str = struct_name.to_string();
7775 let fn_name_str = fn_ident.to_string();
7776 let doc = format!(
7777 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7778 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7779 equivalent to `Self::objects()`. Chains with any \
7780 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7781 extension methods."
7782 );
7783 quote! {
7784 #[doc = #doc]
7785 #[must_use]
7786 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7787 #root::query::QuerySet::new()
7788 }
7789 }
7790 })
7791 .collect();
7792
7793 quote! {
7794 impl #struct_name {
7795 /// Start a new `QuerySet` over this model. Django shape.
7796 #[must_use]
7797 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7798 #root::query::QuerySet::new()
7799 }
7800
7801 /// Eloquent-shape alias of [`Self::objects`]. Returns
7802 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7803 /// `.where_()` / etc. Matches Laravel muscle-memory:
7804 ///
7805 /// ```ignore
7806 /// // Eloquent: Post::query()->where('published', true)
7807 /// // Django: Post.objects.filter(published=True)
7808 /// // rustango: Post::query().filter("published", true)
7809 /// // or: Post::objects().filter("published", true)
7810 /// ```
7811 ///
7812 /// Both names point at the same underlying constructor;
7813 /// neither is preferred.
7814 #[must_use]
7815 pub fn query() -> #root::query::QuerySet<#struct_name> {
7816 #root::query::QuerySet::new()
7817 }
7818
7819 #( #extra_manager_fns )*
7820
7821 #insert_method
7822
7823 #bulk_insert_method
7824
7825 #bulk_upsert_pool_method
7826
7827 #bulk_update_method
7828
7829 #save_method
7830
7831 #pk_methods
7832
7833 #pk_value_helper
7834
7835 #aliased_row_helper
7836
7837 #column_consts
7838 }
7839
7840 #aliased_row_helper_my
7841
7842 #aliased_row_helper_sqlite
7843
7844 #load_related_impl
7845
7846 #load_related_impl_my
7847
7848 #load_related_impl_sqlite
7849
7850 #has_pk_value_impl
7851
7852 #fk_pk_access_impl
7853
7854 #assign_auto_pk_pool_impl
7855 }
7856}
7857
7858/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7859/// `auto_assigns` but reading from `_returning_row` and writing to
7860/// `_row_mut` instead of `self`.
7861fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7862 let root = rustango_root();
7863 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7864 let col_lit = column.as_str();
7865 quote! {
7866 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7867 _returning_row,
7868 #col_lit,
7869 )?;
7870 }
7871 });
7872 quote! { #( #lines )* }
7873}
7874
7875/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7876fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7877 let lines = entries.iter().map(|e| {
7878 let ident = &e.ident;
7879 let col_ty = column_type_ident(ident);
7880 quote! {
7881 #[allow(non_upper_case_globals)]
7882 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7883 }
7884 });
7885 quote! { #(#lines)* }
7886}
7887
7888/// Emit a hidden per-model module carrying one zero-sized type per field,
7889/// each with a `Column` impl pointing back at the model.
7890fn column_module_tokens(
7891 module_ident: &syn::Ident,
7892 struct_name: &syn::Ident,
7893 entries: &[ColumnEntry],
7894) -> TokenStream2 {
7895 let root = rustango_root();
7896 let items = entries.iter().map(|e| {
7897 let col_ty = column_type_ident(&e.ident);
7898 let value_ty = &e.value_ty;
7899 let name = &e.name;
7900 let column = &e.column;
7901 let field_type_tokens = &e.field_type_tokens;
7902 quote! {
7903 #[derive(::core::clone::Clone, ::core::marker::Copy)]
7904 pub struct #col_ty;
7905
7906 impl #root::core::Column for #col_ty {
7907 type Model = super::#struct_name;
7908 type Value = #value_ty;
7909 const NAME: &'static str = #name;
7910 const COLUMN: &'static str = #column;
7911 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7912 }
7913 }
7914 });
7915 quote! {
7916 #[doc(hidden)]
7917 #[allow(non_camel_case_types, non_snake_case)]
7918 pub mod #module_ident {
7919 // Re-import the parent scope so field types referencing
7920 // sibling models (e.g. `ForeignKey<Author>`) resolve
7921 // inside this submodule. Without this we'd hit
7922 // `proc_macro_derive_resolution_fallback` warnings.
7923 #[allow(unused_imports)]
7924 use super::*;
7925 #(#items)*
7926 }
7927 }
7928}
7929
7930fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7931 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7932}
7933
7934fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
7935 syn::Ident::new(
7936 &format!("__rustango_cols_{struct_name}"),
7937 struct_name.span(),
7938 )
7939}
7940
7941fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
7942 let root = rustango_root();
7943 // The Postgres impl is always emitted — every rustango build pulls in
7944 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
7945 // routed through `#root::__impl_my_from_row!`, a cfg-gated
7946 // macro_rules whose body collapses to nothing when rustango is built
7947 // without the `mysql` feature. No user-facing feature shim required.
7948 //
7949 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
7950 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
7951 // back into a comma-separated list inside square brackets. Since each
7952 // entry is already in `field: expr` shape, the existing tokens slot in.
7953 quote! {
7954 #[cfg(feature = "postgres")]
7955 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
7956 for #struct_name
7957 {
7958 fn from_row(
7959 row: &'r #root::sql::sqlx::postgres::PgRow,
7960 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7961 ::core::result::Result::Ok(Self {
7962 #( #from_row_inits ),*
7963 })
7964 }
7965 }
7966
7967 #root::__impl_my_from_row!(#struct_name, |row| {
7968 #( #from_row_inits ),*
7969 });
7970
7971 #root::__impl_sqlite_from_row!(#struct_name, |row| {
7972 #( #from_row_inits ),*
7973 });
7974 }
7975}
7976
7977struct ContainerAttrs {
7978 table: Option<String>,
7979 display: Option<(String, proc_macro2::Span)>,
7980 /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
7981 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
7982 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
7983 /// at runtime — this attribute is the override for cases where
7984 /// the inference is wrong (e.g. a model that conceptually belongs
7985 /// to one app but is physically in another module).
7986 app: Option<String>,
7987 /// Django ModelAdmin-shape per-model knobs from
7988 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
7989 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
7990 /// admin code falls back to `AdminConfig::DEFAULT`.
7991 admin: Option<AdminAttrs>,
7992 /// Per-model audit configuration from `#[rustango(audit(...))]`.
7993 /// `None` when the model isn't audited — write paths emit no
7994 /// audit entries. When present, single-row writes capture
7995 /// before/after for the listed fields and bulk writes batch
7996 /// snapshots into one INSERT into `rustango_audit_log`.
7997 audit: Option<AuditAttrs>,
7998 /// `true` when `#[rustango(permissions)]` is present. Signals that
7999 /// `auto_create_permissions` should seed the four CRUD codenames for
8000 /// this model.
8001 permissions: bool,
8002 /// Many-to-many relations declared via
8003 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8004 /// src = "post_id", dst = "tag_id"))]`.
8005 m2m: Vec<M2MAttr>,
8006 /// Polymorphic M2M relations declared via
8007 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8008 /// pk_column = "taggable_id", ct_column = "taggable_type",
8009 /// related_column = "tag_id"))]` (issue #818).
8010 generic_m2m: Vec<GenericM2MAttr>,
8011 /// Composite indexes declared via
8012 /// `#[rustango(index("col1, col2"))]` or
8013 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8014 /// Single-column indexes from `#[rustango(index)]` on fields are
8015 /// accumulated here during field collection.
8016 indexes: Vec<IndexAttr>,
8017 /// Table-level CHECK constraints declared via
8018 /// `#[rustango(check(name = "…", expr = "…"))]`.
8019 checks: Vec<CheckAttr>,
8020 /// Table-level PG `EXCLUDE` constraints declared via
8021 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8022 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8023 /// migration writer renders them on Postgres and skips with a
8024 /// warning on MySQL/SQLite. Issue #319.
8025 excludes: Vec<ExcludeAttr>,
8026 /// Composite (multi-column) FKs declared via
8027 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8028 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8029 composite_fks: Vec<CompositeFkAttr>,
8030 /// Generic ("any model") FKs declared via
8031 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8032 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8033 generic_fks: Vec<GenericFkAttr>,
8034 /// Where this model lives in a tenancy deployment, declared via
8035 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8036 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8037 /// to partition diff output between registry-scoped and
8038 /// tenant-scoped migration files.
8039 scope: Option<String>,
8040 /// Custom-Manager extension-trait name from
8041 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8042 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8043 /// adjacent to the model so users can write
8044 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8045 /// and discover the convention from the model definition.
8046 manager_ext: Option<syn::Ident>,
8047 /// Extra QuerySet accessor names from
8048 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8049 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8050 /// the default `Self::objects()`. Multiple attributes allowed.
8051 manager_fns: Vec<syn::Ident>,
8052 /// Default ordering declared via `#[rustango(default_order =
8053 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8054 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8055 /// `-` prefix means descending; the `+` prefix or no prefix means
8056 /// ascending.
8057 default_order: Vec<(String, bool, proc_macro2::Span)>,
8058 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8059 /// Routes the emitted schema's `is_view = true` so the migration
8060 /// snapshot skips this model (its underlying SQL view is operator-
8061 /// managed, not rustango-managed).
8062 is_view: bool,
8063 /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8064 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8065 /// the migration snapshot skips this model so `makemigrations` /
8066 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8067 /// TABLE` against it (operator-managed schema).
8068 managed: bool,
8069 /// Django-shape `Meta.base_manager_name` from
8070 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8071 /// `ModelSchema::base_manager_name`. Declarative-only today.
8072 base_manager_name: Option<String>,
8073 /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8074 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8075 /// field this model's instances are ordered relative to.
8076 /// Declarative-only today; threaded onto
8077 /// `ModelSchema::order_with_respect_to`.
8078 order_with_respect_to: Option<String>,
8079 /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8080 /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8081 /// shares its DB table with another struct. Threaded into
8082 /// `ModelSchema::proxy` so future codegen can skip table-owning
8083 /// behavior for proxies.
8084 proxy: bool,
8085 /// Django-shape `Meta.required_db_features` from
8086 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8087 /// Each comma-separated capability token surfaces on
8088 /// `ModelSchema::required_db_features` so `manage check --deploy`
8089 /// can warn when the active dialect lacks one.
8090 required_db_features: Vec<String>,
8091 /// Django-shape `Meta.required_db_vendor` from
8092 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8093 /// Normalized to the dialect name `manage check --deploy`
8094 /// compares against `Settings.database.backend`. Aliases
8095 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8096 /// stored under the canonical name.
8097 required_db_vendor: Option<String>,
8098 /// Django-shape `Meta.default_related_name` from
8099 /// `#[rustango(default_related_name = "...")]`. Threaded into
8100 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8101 /// name to use when an FK / M2M field doesn't override it.
8102 /// Today rustango doesn't auto-emit reverse managers; the
8103 /// metadata is the foundation for that work.
8104 default_related_name: Option<String>,
8105 /// Django-shape `Meta.db_table_comment` (4.2+) from
8106 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8107 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8108 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8109 /// inline `COMMENT='...'`, SQLite: no-op).
8110 db_table_comment: Option<String>,
8111 /// Django-shape `Meta.get_latest_by` from
8112 /// `#[rustango(get_latest_by = "created_at")]` /
8113 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8114 /// `(column, descending)` where `descending = true` when the
8115 /// attribute value starts with `-`. Threaded into
8116 /// `ModelSchema::get_latest_by`.
8117 get_latest_by: Option<(String, bool)>,
8118 /// Django-shape `Meta.permissions = [(codename, name), ...]`
8119 /// from `#[rustango(extra_permissions = "approve:Can approve,
8120 /// archive:Can archive")]`. Comma-separated `codename:label`
8121 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8122 extra_permissions: Vec<(String, String)>,
8123 /// Django-shape `Meta.default_permissions` — which CRUD codenames
8124 /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8125 /// auto-creates. Empty `Vec` (default) means **all four** — matches
8126 /// Django's behavior when the operator omits the option. Set via
8127 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8128 /// Validated at parse time; unknown actions fail with a span-pointing
8129 /// error.
8130 default_permissions: Vec<String>,
8131 /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8132 /// human-readable singular label for the model. Threaded into
8133 /// `ModelSchema::verbose_name` so admin section headers /
8134 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8135 /// over the Rust struct identifier.
8136 verbose_name: Option<String>,
8137 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8138 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8139 /// When unset, `display_label_plural()` auto-suffixes `s`.
8140 verbose_name_plural: Option<String>,
8141 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8142 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8143 /// a name (used by `QuerySet::without_global_scope`) with a
8144 /// `fn() -> WhereExpr` path that the macro emits into
8145 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8146 global_scopes: Vec<GlobalScopeAttr>,
8147 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8148 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8149 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8150 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8151 /// entry emits an inherent `<name>_through(&self)` accessor that
8152 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8153 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8154 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8155 through_relations: Vec<ThroughAttr>,
8156 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8157 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8158 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8159 /// Each entry emits two associated functions on the parent —
8160 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8161 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8162 /// over a correlated subquery against the child table. Users
8163 /// drop the result into `QuerySet::where_raw(...)`.
8164 reverse_has_relations: Vec<ReverseHasAttr>,
8165 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8166 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8167 /// entry so the relation-existence family resolves polymorphic
8168 /// children by name.
8169 generic_has_relations: Vec<GenericHasAttr>,
8170}
8171
8172/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8173/// declaration. Each entry becomes one `core::GlobalScope` in the
8174/// emitted schema literal; `apply` resolves at macro-expand time
8175/// against the consumer's scope so the function must be in scope at
8176/// the use site. Issue #820.
8177struct GlobalScopeAttr {
8178 name: String,
8179 apply: syn::Path,
8180}
8181
8182/// Parsed `#[rustango(through(...))]` declaration. Issue
8183/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8184/// `hasManyThrough` / `hasOneThrough` parity.
8185///
8186/// `Country hasManyThrough Post via User` declares as:
8187///
8188/// ```ignore
8189/// #[rustango(through(
8190/// name = "posts",
8191/// far = "Post",
8192/// far_fk_column = "author_id",
8193/// intermediate = "User",
8194/// intermediate_fk_column = "country_id",
8195/// ))]
8196/// struct Country { ... }
8197/// ```
8198///
8199/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8200/// which returns a queryset filtered via a correlated subquery —
8201/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8202/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8203/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8204/// because the subquery lives inside a `WhereExpr::InSubquery` node
8205/// and the outer queryset's pending list stays empty.
8206///
8207/// All four required arguments use **SQL column / table names**
8208/// (not Rust field names) to sidestep the multi-hop-filter substrate
8209/// gap. Once that substrate lands, a higher-level Rust-field-name
8210/// shorthand can be added without breaking this surface.
8211struct ThroughAttr {
8212 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8213 name: String,
8214 /// Far model type identifier. `far = "Post"` → returns
8215 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8216 /// the derive expands.
8217 far: syn::Ident,
8218 /// SQL column on the far model's table that references the
8219 /// intermediate model's primary key. For `Post`'s
8220 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8221 /// default `<field>_id` convention) or whatever the user
8222 /// declared via `#[rustango(db_column = "...")]`.
8223 far_fk_column: String,
8224 /// Intermediate model type identifier. Needed to look up its
8225 /// `SCHEMA` so the subquery's `FROM` clause points at the
8226 /// intermediate table. `intermediate = "User"`.
8227 intermediate: syn::Ident,
8228 /// SQL column on the intermediate's table that references the
8229 /// source (this) model's primary key. For `User`'s
8230 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8231 intermediate_fk_column: String,
8232 /// SQL primary-key column on the intermediate's table — the
8233 /// column the subquery projects. Optional; defaults to `"id"`
8234 /// (rustango's default PK column name). Override when the
8235 /// intermediate declares a custom PK column.
8236 intermediate_pk_column: String,
8237}
8238
8239/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8240/// child_fk_column = "..."))]` declaration. Issue
8241/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8242/// `whereHas` / `whereDoesntHave` parity.
8243///
8244/// `Post hasMany Comment` declares as:
8245///
8246/// ```ignore
8247/// #[rustango(reverse_has(
8248/// name = "comments",
8249/// child = "Comment",
8250/// child_fk_column = "post_id",
8251/// ))]
8252/// struct Post { ... }
8253/// ```
8254///
8255/// The macro emits two associated functions on `Post`:
8256///
8257/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8258/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8259/// <outer>.<self_pk_column>)`.
8260/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8261/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8262///
8263/// User code:
8264///
8265/// ```ignore
8266/// // Posts with at least one comment:
8267/// Post::objects().where_raw(Post::comments_exists_expr()).fetch_pool(&pool)
8268/// // Posts with no comments:
8269/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch_pool(&pool)
8270/// ```
8271///
8272/// As with #817, all column / table identifiers are **SQL names**
8273/// (not Rust field names) so the substrate is independent of the
8274/// outstanding multi-hop filter resolver gap. The emitted
8275/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8276/// SQL-emit time via the writer's scope stack.
8277struct ReverseHasAttr {
8278 /// Accessor name. `name = "comments"` → emits
8279 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8280 name: String,
8281 /// Child model type identifier. `child = "Comment"` — needed to
8282 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8283 /// points at the child table.
8284 child: syn::Ident,
8285 /// SQL column on the child's table that references this model's
8286 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8287 /// column is `"post_id"`.
8288 child_fk_column: String,
8289 /// SQL primary-key column on **this** model's table — the column
8290 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8291 /// (rustango's default PK column name).
8292 self_pk_column: String,
8293}
8294
8295/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8296/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8297/// relation-existence family (issue #830). The child is a polymorphic,
8298/// content-type-discriminated model; emits a `Model::
8299/// generic_reverse_relations()` entry the queryset resolves by name.
8300struct GenericHasAttr {
8301 /// Accessor name, e.g. `name = "tags"`.
8302 name: String,
8303 /// Child model type identifier (`child = "Tag"`) — looked up for its
8304 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8305 child: syn::Ident,
8306 /// Column on the child table holding the parent's content-type id.
8307 /// Optional; defaults to `"content_type_id"`.
8308 ct_column: String,
8309 /// Column on the child table holding the parent's PK value.
8310 /// Optional; defaults to `"object_pk"`.
8311 pk_column: String,
8312 /// SQL primary-key column on **this** (parent) model's table.
8313 /// Optional; defaults to `"id"`.
8314 self_pk_column: String,
8315}
8316
8317/// Parsed form of one index declaration (field-level or container-level).
8318struct IndexAttr {
8319 /// Index name; auto-derived when `None` at parse time.
8320 name: Option<String>,
8321 /// Column names in the index.
8322 columns: Vec<String>,
8323 /// `true` for `CREATE UNIQUE INDEX`.
8324 unique: bool,
8325 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8326 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8327 /// `"btree"` when the attribute is absent — the DDL writer omits
8328 /// the `USING` clause and the backend uses its own default
8329 /// (btree on every supported dialect).
8330 method: String,
8331 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8332 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8333 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8334 where_clause: Option<String>,
8335 /// Django `Index(fields=..., include=[...])` covering-index
8336 /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8337 /// default) means "no covering columns".
8338 include: Vec<String>,
8339}
8340
8341/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8342struct CheckAttr {
8343 name: String,
8344 expr: String,
8345}
8346
8347/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8348/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8349/// PG-only — surfaced on every backend in the macro emit; the migration
8350/// writer skips the op on MySQL/SQLite. Issue #319.
8351struct ExcludeAttr {
8352 /// Constraint name (free-form Rust identifier).
8353 name: String,
8354 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8355 using: String,
8356 /// Comma-separated `(column, operator)` pairs, in declaration
8357 /// order. Parsed from `"col WITH op, col WITH op"`.
8358 elements: Vec<(String, String)>,
8359 /// Optional `WHERE` predicate (raw SQL).
8360 where_clause: Option<String>,
8361}
8362
8363/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8364/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8365/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8366/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8367/// the model, not the field.
8368struct CompositeFkAttr {
8369 /// Logical relation name (free-form Rust identifier).
8370 name: String,
8371 /// SQL table name of the target.
8372 to: String,
8373 /// Source-side column names, in declaration order.
8374 from: Vec<String>,
8375 /// Target-side column names, same length / order as `from`.
8376 on: Vec<String>,
8377}
8378
8379/// Parsed form of one `#[rustango(generic_fk(name = "target",
8380/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8381/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8382/// generic ("any model") FKs live on the model, not the field.
8383struct GenericFkAttr {
8384 /// Logical relation name (free-form Rust identifier).
8385 name: String,
8386 /// Source-side column carrying the `content_type_id` value.
8387 ct_column: String,
8388 /// Source-side column carrying the target row's primary key.
8389 pk_column: String,
8390}
8391
8392/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8393struct M2MAttr {
8394 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8395 name: String,
8396 /// Target table (e.g. `"app_tags"`).
8397 to: String,
8398 /// Junction table (e.g. `"post_tags"`).
8399 through: String,
8400 /// Source FK column in the junction table (e.g. `"post_id"`).
8401 src: String,
8402 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8403 dst: String,
8404 /// Whether the migration writer should auto-create the junction
8405 /// table. Default `true`. Set `auto_create = false` (#324) when
8406 /// the operator declares the through table as its own
8407 /// `#[derive(Model)]` struct with extra columns.
8408 auto_create: bool,
8409}
8410
8411/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8412/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8413/// junction carries a ContentType discriminator so unrelated models
8414/// share one pivot + related set.
8415struct GenericM2MAttr {
8416 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8417 name: String,
8418 /// Polymorphic junction table (e.g. `"taggables"`).
8419 through: String,
8420 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8421 pk_column: String,
8422 /// Junction column holding the owning model's `content_type_id`
8423 /// discriminator (e.g. `"taggable_type"`).
8424 ct_column: String,
8425 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8426 related_column: String,
8427}
8428
8429/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8430/// "user"))]`. `track` is a comma-separated list of field names whose
8431/// before/after values land in the JSONB `changes` column. `source`
8432/// is informational only — it pins a default source when the model
8433/// is written outside any `audit::with_source(...)` scope (rare).
8434#[derive(Default)]
8435struct AuditAttrs {
8436 /// Field names to capture in the `changes` JSONB. Validated
8437 /// against declared scalar fields at compile time. Empty means
8438 /// "track every scalar field" — Django's audit-everything default.
8439 track: Option<(Vec<String>, proc_macro2::Span)>,
8440}
8441
8442/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8443/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8444/// comma-separated strings; we validate each ident against the model's
8445/// declared fields at compile time.
8446#[derive(Default)]
8447struct AdminAttrs {
8448 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8449 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8450 list_per_page: Option<usize>,
8451 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8452 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8453 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8454 /// Bulk action names. No field-validation against model fields —
8455 /// these are action handlers, not column references.
8456 actions: Option<(Vec<String>, proc_macro2::Span)>,
8457 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8458 /// sections, comma-separated fields per section, optional
8459 /// `Title:` prefix. Empty title omits the `<legend>`.
8460 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8461 /// `admin(list_display_links = "title")` — Django-shape. Names
8462 /// from `list_display` whose cells should link to detail/edit.
8463 /// Issue #350.
8464 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8465 /// `admin(search_help_text = "...")` — Django-shape. Short
8466 /// caption rendered beside the admin list view's search box.
8467 /// Issue #353.
8468 search_help_text: Option<String>,
8469 /// `admin(actions_on_top = false)` — Django-shape. Hides the
8470 /// action-bar above the table. Default `true`. Issue #354.
8471 actions_on_top: Option<bool>,
8472 /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8473 /// additional action-bar below the table. Default `false`.
8474 /// Issue #354.
8475 actions_on_bottom: Option<bool>,
8476 /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8477 /// a date / datetime field whose values render as a clickable
8478 /// year / month / day drill-down strip above the list table.
8479 /// Empty / unset disables the strip. Issue #355.
8480 date_hierarchy: Option<String>,
8481 /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8482 /// Each entry is `target:source[+source2]`; multiple entries are
8483 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8484 /// The admin change-form emits JS that slugifies the source values
8485 /// into the target field on every keystroke. Issue #356.
8486 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8487 /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8488 /// of FK fields whose change-form widget renders a lookup link
8489 /// next to the input. Issue #357.
8490 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8491 /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8492 /// Names of FK fields whose change-form widget renders an
8493 /// Ajax-driven typeahead populated from a `__autocomplete`
8494 /// endpoint on the target model. Issue #358.
8495 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8496 /// `admin(list_select_related = "all" | "none" | "author, …")`
8497 /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8498 /// policy. Default `"all"` matches rustango's join-everything
8499 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8500 list_select_related: Option<String>,
8501 /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8502 /// Django-shape. Each entry is `field_name:widget_name`; multiple
8503 /// entries comma-separated. Empty / unset → no overrides. The
8504 /// list of widget names supported is documented on
8505 /// `AdminConfig::formfield_overrides`. Issue #359.
8506 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8507}
8508
8509fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8510 let mut out = ContainerAttrs {
8511 table: None,
8512 display: None,
8513 app: None,
8514 admin: None,
8515 audit: None,
8516 // Default `permissions = true` so every `#[derive(Model)]`
8517 // gets the four CRUD codenames seeded by `auto_create_permissions`
8518 // and is visible to non-superusers in the tenant admin without
8519 // manual per-model annotation. Models that intentionally don't
8520 // want permission rows (registry-internal types, framework
8521 // tables operators shouldn't manage directly) opt out via
8522 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8523 // out-of-the-box admin invisibility regression (#62).
8524 permissions: true,
8525 m2m: Vec::new(),
8526 generic_m2m: Vec::new(),
8527 indexes: Vec::new(),
8528 checks: Vec::new(),
8529 excludes: Vec::new(),
8530 composite_fks: Vec::new(),
8531 generic_fks: Vec::new(),
8532 scope: None,
8533 manager_ext: None,
8534 manager_fns: Vec::new(),
8535 default_order: Vec::new(),
8536 is_view: false,
8537 managed: true,
8538 verbose_name: None,
8539 verbose_name_plural: None,
8540 base_manager_name: None,
8541 order_with_respect_to: None,
8542 proxy: false,
8543 required_db_features: Vec::new(),
8544 required_db_vendor: None,
8545 default_related_name: None,
8546 db_table_comment: None,
8547 get_latest_by: None,
8548 extra_permissions: Vec::new(),
8549 default_permissions: Vec::new(),
8550 global_scopes: Vec::new(),
8551 through_relations: Vec::new(),
8552 reverse_has_relations: Vec::new(),
8553 generic_has_relations: Vec::new(),
8554 };
8555 for attr in &input.attrs {
8556 if !attr.path().is_ident("rustango") {
8557 continue;
8558 }
8559 attr.parse_nested_meta(|meta| {
8560 if meta.path.is_ident("table") {
8561 let s: LitStr = meta.value()?.parse()?;
8562 let name = s.value();
8563 // v0.27.3 (#65) — macro-time guard against table names
8564 // that compile but break SQL downstream. Hyphens are
8565 // the common footgun: PostgreSQL accepts a quoted
8566 // `"intermediate-region"` in CREATE TABLE, but the
8567 // FK / index name derivation in `migrate::ddl`
8568 // emits `intermediate-region_field_fkey` unquoted,
8569 // which then fails the SQL parser. Same shape rule
8570 // as Postgres regular identifiers so the safe path
8571 // is the only path.
8572 validate_table_name(&name, s.span())?;
8573 out.table = Some(name);
8574 return Ok(());
8575 }
8576 if meta.path.is_ident("display") {
8577 let s: LitStr = meta.value()?.parse()?;
8578 out.display = Some((s.value(), s.span()));
8579 return Ok(());
8580 }
8581 if meta.path.is_ident("app") {
8582 let s: LitStr = meta.value()?.parse()?;
8583 out.app = Some(s.value());
8584 return Ok(());
8585 }
8586 if meta.path.is_ident("scope") {
8587 let s: LitStr = meta.value()?.parse()?;
8588 let val = s.value();
8589 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8590 return Err(meta.error(format!(
8591 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8592 )));
8593 }
8594 out.scope = Some(val);
8595 return Ok(());
8596 }
8597 if meta.path.is_ident("admin") {
8598 let mut admin = AdminAttrs::default();
8599 meta.parse_nested_meta(|inner| {
8600 if inner.path.is_ident("list_display") {
8601 let s: LitStr = inner.value()?.parse()?;
8602 admin.list_display =
8603 Some((split_field_list(&s.value()), s.span()));
8604 return Ok(());
8605 }
8606 if inner.path.is_ident("search_fields") {
8607 let s: LitStr = inner.value()?.parse()?;
8608 admin.search_fields =
8609 Some((split_field_list(&s.value()), s.span()));
8610 return Ok(());
8611 }
8612 if inner.path.is_ident("readonly_fields") {
8613 let s: LitStr = inner.value()?.parse()?;
8614 admin.readonly_fields =
8615 Some((split_field_list(&s.value()), s.span()));
8616 return Ok(());
8617 }
8618 if inner.path.is_ident("list_per_page") {
8619 let lit: syn::LitInt = inner.value()?.parse()?;
8620 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8621 return Ok(());
8622 }
8623 if inner.path.is_ident("ordering") {
8624 let s: LitStr = inner.value()?.parse()?;
8625 admin.ordering = Some((
8626 parse_ordering_list(&s.value()),
8627 s.span(),
8628 ));
8629 return Ok(());
8630 }
8631 if inner.path.is_ident("list_filter") {
8632 let s: LitStr = inner.value()?.parse()?;
8633 admin.list_filter =
8634 Some((split_field_list(&s.value()), s.span()));
8635 return Ok(());
8636 }
8637 if inner.path.is_ident("actions") {
8638 let s: LitStr = inner.value()?.parse()?;
8639 admin.actions =
8640 Some((split_field_list(&s.value()), s.span()));
8641 return Ok(());
8642 }
8643 if inner.path.is_ident("fieldsets") {
8644 let s: LitStr = inner.value()?.parse()?;
8645 admin.fieldsets =
8646 Some((parse_fieldset_list(&s.value()), s.span()));
8647 return Ok(());
8648 }
8649 if inner.path.is_ident("list_display_links") {
8650 let s: LitStr = inner.value()?.parse()?;
8651 admin.list_display_links =
8652 Some((split_field_list(&s.value()), s.span()));
8653 return Ok(());
8654 }
8655 if inner.path.is_ident("search_help_text") {
8656 let s: LitStr = inner.value()?.parse()?;
8657 admin.search_help_text = Some(s.value());
8658 return Ok(());
8659 }
8660 if inner.path.is_ident("actions_on_top") {
8661 let lit: syn::LitBool = inner.value()?.parse()?;
8662 admin.actions_on_top = Some(lit.value);
8663 return Ok(());
8664 }
8665 if inner.path.is_ident("actions_on_bottom") {
8666 let lit: syn::LitBool = inner.value()?.parse()?;
8667 admin.actions_on_bottom = Some(lit.value);
8668 return Ok(());
8669 }
8670 if inner.path.is_ident("date_hierarchy") {
8671 let s: LitStr = inner.value()?.parse()?;
8672 admin.date_hierarchy = Some(s.value());
8673 return Ok(());
8674 }
8675 if inner.path.is_ident("prepopulated_fields") {
8676 let s: LitStr = inner.value()?.parse()?;
8677 admin.prepopulated_fields =
8678 Some((parse_prepopulated_list(&s.value()), s.span()));
8679 return Ok(());
8680 }
8681 if inner.path.is_ident("raw_id_fields") {
8682 let s: LitStr = inner.value()?.parse()?;
8683 admin.raw_id_fields =
8684 Some((split_field_list(&s.value()), s.span()));
8685 return Ok(());
8686 }
8687 if inner.path.is_ident("autocomplete_fields") {
8688 let s: LitStr = inner.value()?.parse()?;
8689 admin.autocomplete_fields =
8690 Some((split_field_list(&s.value()), s.span()));
8691 return Ok(());
8692 }
8693 if inner.path.is_ident("list_select_related") {
8694 let s: LitStr = inner.value()?.parse()?;
8695 admin.list_select_related = Some(s.value());
8696 return Ok(());
8697 }
8698 if inner.path.is_ident("formfield_overrides") {
8699 let s: LitStr = inner.value()?.parse()?;
8700 admin.formfield_overrides =
8701 Some((parse_formfield_overrides(&s.value()), s.span()));
8702 return Ok(());
8703 }
8704 Err(inner.error(
8705 "unknown admin attribute (supported: \
8706 `list_display`, `list_display_links`, \
8707 `search_fields`, `search_help_text`, \
8708 `readonly_fields`, \
8709 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8710 `actions_on_top`, `actions_on_bottom`, \
8711 `date_hierarchy`, \
8712 `prepopulated_fields`, \
8713 `raw_id_fields`, \
8714 `autocomplete_fields`, \
8715 `list_select_related`, \
8716 `formfield_overrides`, \
8717 `fieldsets`)",
8718 ))
8719 })?;
8720 out.admin = Some(admin);
8721 return Ok(());
8722 }
8723 if meta.path.is_ident("manager") {
8724 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8725 // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8726 // shape) is left as a follow-up — the issue's primary
8727 // acceptance is the `ext = ...` trait emission.
8728 meta.parse_nested_meta(|inner| {
8729 if inner.path.is_ident("ext") {
8730 let s: LitStr = inner.value()?.parse()?;
8731 let name = s.value();
8732 if name.is_empty() {
8733 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8734 }
8735 out.manager_ext =
8736 Some(syn::Ident::new(&name, s.span()));
8737 return Ok(());
8738 }
8739 Err(inner.error(
8740 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8741 ))
8742 })?;
8743 return Ok(());
8744 }
8745 if meta.path.is_ident("manager_fn") {
8746 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8747 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8748 // next to the default `Self::objects()`. Multiple
8749 // attributes accumulate.
8750 let s: LitStr = meta.value()?.parse()?;
8751 let name = s.value();
8752 if name.is_empty() {
8753 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8754 }
8755 if name == "objects" {
8756 return Err(meta.error(
8757 "`manager_fn = \"objects\"` collides with the default \
8758 accessor — pick a different name",
8759 ));
8760 }
8761 let ident = syn::Ident::new(&name, s.span());
8762 if out.manager_fns.iter().any(|prev| *prev == ident) {
8763 return Err(meta.error(format!(
8764 "duplicate `manager_fn = \"{name}\"`"
8765 )));
8766 }
8767 out.manager_fns.push(ident);
8768 return Ok(());
8769 }
8770 if meta.path.is_ident("default_order") {
8771 // `#[rustango(default_order = "-created_at, status")]`
8772 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8773 // means descending, `+prefix` or bare name means ascending.
8774 // Per-query opt-in via `QuerySet::with_default_order()`.
8775 let s: LitStr = meta.value()?.parse()?;
8776 let raw = s.value();
8777 let span = s.span();
8778 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8779 Vec::new();
8780 for entry in raw.split(',') {
8781 let trimmed = entry.trim();
8782 if trimmed.is_empty() {
8783 return Err(syn::Error::new(
8784 span,
8785 "`default_order = \"...\"` has an empty entry — \
8786 check for a stray comma",
8787 ));
8788 }
8789 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8790 (true, rest.trim().to_owned())
8791 } else if let Some(rest) = trimmed.strip_prefix('+') {
8792 (false, rest.trim().to_owned())
8793 } else {
8794 (false, trimmed.to_owned())
8795 };
8796 if name.is_empty() {
8797 return Err(syn::Error::new(
8798 span,
8799 "`default_order` entry has no column name after the prefix",
8800 ));
8801 }
8802 if parsed.iter().any(|(n, _, _)| *n == name) {
8803 return Err(syn::Error::new(
8804 span,
8805 format!("duplicate column `{name}` in `default_order`"),
8806 ));
8807 }
8808 parsed.push((name, desc, span));
8809 }
8810 if parsed.is_empty() {
8811 return Err(syn::Error::new(
8812 span,
8813 "`default_order = \"...\"` cannot be empty",
8814 ));
8815 }
8816 out.default_order = parsed;
8817 return Ok(());
8818 }
8819 if meta.path.is_ident("global_scope") {
8820 // `#[rustango(global_scope(name = "active", apply =
8821 // path::to::fn))]` — issue #820. The apply function
8822 // path resolves at macro-expand time in the consumer's
8823 // scope; the macro re-emits it verbatim into the
8824 // `ModelSchema::global_scopes` slice literal.
8825 let span = meta.path.span();
8826 let mut scope_name: Option<String> = None;
8827 let mut apply_path: Option<syn::Path> = None;
8828 meta.parse_nested_meta(|inner| {
8829 if inner.path.is_ident("name") {
8830 let s: LitStr = inner.value()?.parse()?;
8831 let raw = s.value();
8832 if raw.trim().is_empty() {
8833 return Err(syn::Error::new(
8834 s.span(),
8835 "`global_scope(name = \"...\")` must not be empty",
8836 ));
8837 }
8838 scope_name = Some(raw);
8839 return Ok(());
8840 }
8841 if inner.path.is_ident("apply") {
8842 let p: syn::Path = inner.value()?.parse()?;
8843 apply_path = Some(p);
8844 return Ok(());
8845 }
8846 Err(inner.error(
8847 "unknown `global_scope` attribute (supported: \
8848 `name`, `apply`)",
8849 ))
8850 })?;
8851 let Some(name) = scope_name else {
8852 return Err(syn::Error::new(
8853 span,
8854 "`global_scope` requires `name = \"...\"`",
8855 ));
8856 };
8857 let Some(apply) = apply_path else {
8858 return Err(syn::Error::new(
8859 span,
8860 "`global_scope` requires `apply = fn_path`",
8861 ));
8862 };
8863 if out.global_scopes.iter().any(|s| s.name == name) {
8864 return Err(syn::Error::new(
8865 span,
8866 format!(
8867 "duplicate global scope name `{name}` — \
8868 pick a unique identifier so \
8869 `QuerySet::without_global_scope(\"{name}\")` \
8870 is unambiguous"
8871 ),
8872 ));
8873 }
8874 out.global_scopes.push(GlobalScopeAttr { name, apply });
8875 return Ok(());
8876 }
8877 if meta.path.is_ident("through") {
8878 // `#[rustango(through(name = "posts", far = "Post",
8879 // far_fk_column = "author_id", intermediate = "User",
8880 // intermediate_fk_column = "country_id"
8881 // [, intermediate_pk_column = "id"]))]` — issue #817.
8882 let span = meta.path.span();
8883 let mut accessor_name: Option<String> = None;
8884 let mut far_ident: Option<syn::Ident> = None;
8885 let mut far_fk_column: Option<String> = None;
8886 let mut intermediate_ident: Option<syn::Ident> = None;
8887 let mut intermediate_fk_column: Option<String> = None;
8888 let mut intermediate_pk_column: Option<String> = None;
8889 fn parse_nonempty_string(
8890 inner: &syn::meta::ParseNestedMeta<'_>,
8891 field: &str,
8892 ) -> syn::Result<String> {
8893 let s: LitStr = inner.value()?.parse()?;
8894 let raw = s.value();
8895 let trimmed = raw.trim();
8896 if trimmed.is_empty() {
8897 return Err(syn::Error::new(
8898 s.span(),
8899 format!("`through({field} = \"...\")` must not be empty"),
8900 ));
8901 }
8902 Ok(trimmed.to_owned())
8903 }
8904 meta.parse_nested_meta(|inner| {
8905 if inner.path.is_ident("name") {
8906 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8907 return Ok(());
8908 }
8909 if inner.path.is_ident("far") {
8910 // Far model name accepted as a string literal
8911 // so the attribute fits inside the existing
8912 // `parse_nested_meta` shape; parsed back into a
8913 // `syn::Ident` so the emitted accessor can
8914 // reference the type directly.
8915 let s: LitStr = inner.value()?.parse()?;
8916 let raw = s.value();
8917 let trimmed = raw.trim();
8918 if trimmed.is_empty() {
8919 return Err(syn::Error::new(
8920 s.span(),
8921 "`through(far = \"...\")` must not be empty",
8922 ));
8923 }
8924 far_ident = Some(syn::Ident::new(trimmed, s.span()));
8925 return Ok(());
8926 }
8927 if inner.path.is_ident("far_fk_column") {
8928 far_fk_column =
8929 Some(parse_nonempty_string(&inner, "far_fk_column")?);
8930 return Ok(());
8931 }
8932 if inner.path.is_ident("intermediate") {
8933 let s: LitStr = inner.value()?.parse()?;
8934 let raw = s.value();
8935 let trimmed = raw.trim();
8936 if trimmed.is_empty() {
8937 return Err(syn::Error::new(
8938 s.span(),
8939 "`through(intermediate = \"...\")` must not be empty",
8940 ));
8941 }
8942 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
8943 return Ok(());
8944 }
8945 if inner.path.is_ident("intermediate_fk_column") {
8946 intermediate_fk_column =
8947 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
8948 return Ok(());
8949 }
8950 if inner.path.is_ident("intermediate_pk_column") {
8951 intermediate_pk_column =
8952 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
8953 return Ok(());
8954 }
8955 Err(inner.error(
8956 "unknown `through` attribute (supported: \
8957 `name`, `far`, `far_fk_column`, \
8958 `intermediate`, `intermediate_fk_column`, \
8959 `intermediate_pk_column`)",
8960 ))
8961 })?;
8962 let Some(name) = accessor_name else {
8963 return Err(syn::Error::new(
8964 span,
8965 "`through` requires `name = \"...\"`",
8966 ));
8967 };
8968 let Some(far) = far_ident else {
8969 return Err(syn::Error::new(
8970 span,
8971 "`through` requires `far = \"FarModelType\"`",
8972 ));
8973 };
8974 let Some(far_fk_column) = far_fk_column else {
8975 return Err(syn::Error::new(
8976 span,
8977 "`through` requires `far_fk_column = \"<column>\"`",
8978 ));
8979 };
8980 let Some(intermediate) = intermediate_ident else {
8981 return Err(syn::Error::new(
8982 span,
8983 "`through` requires `intermediate = \"IntermediateModelType\"`",
8984 ));
8985 };
8986 let Some(intermediate_fk_column) = intermediate_fk_column else {
8987 return Err(syn::Error::new(
8988 span,
8989 "`through` requires `intermediate_fk_column = \"<column>\"`",
8990 ));
8991 };
8992 let intermediate_pk_column =
8993 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
8994 if out.through_relations.iter().any(|t| t.name == name) {
8995 return Err(syn::Error::new(
8996 span,
8997 format!(
8998 "duplicate `through(name = \"{name}\")` — \
8999 pick a unique accessor name"
9000 ),
9001 ));
9002 }
9003 out.through_relations.push(ThroughAttr {
9004 name,
9005 far,
9006 far_fk_column,
9007 intermediate,
9008 intermediate_fk_column,
9009 intermediate_pk_column,
9010 });
9011 return Ok(());
9012 }
9013 if meta.path.is_ident("reverse_has") {
9014 // `#[rustango(reverse_has(name = "comments",
9015 // child = "Comment", child_fk_column = "post_id"
9016 // [, self_pk_column = "id"]))]` — issue #830.
9017 let span = meta.path.span();
9018 let mut accessor_name: Option<String> = None;
9019 let mut child_ident: Option<syn::Ident> = None;
9020 let mut child_fk_column: Option<String> = None;
9021 let mut self_pk_column: Option<String> = None;
9022 meta.parse_nested_meta(|inner| {
9023 if inner.path.is_ident("name") {
9024 let s: LitStr = inner.value()?.parse()?;
9025 let raw = s.value();
9026 if raw.trim().is_empty() {
9027 return Err(syn::Error::new(
9028 s.span(),
9029 "`reverse_has(name = \"...\")` must not be empty",
9030 ));
9031 }
9032 accessor_name = Some(raw);
9033 return Ok(());
9034 }
9035 if inner.path.is_ident("child") {
9036 let s: LitStr = inner.value()?.parse()?;
9037 let raw = s.value();
9038 let trimmed = raw.trim();
9039 if trimmed.is_empty() {
9040 return Err(syn::Error::new(
9041 s.span(),
9042 "`reverse_has(child = \"...\")` must not be empty",
9043 ));
9044 }
9045 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9046 return Ok(());
9047 }
9048 if inner.path.is_ident("child_fk_column") {
9049 let s: LitStr = inner.value()?.parse()?;
9050 let raw = s.value();
9051 let trimmed = raw.trim();
9052 if trimmed.is_empty() {
9053 return Err(syn::Error::new(
9054 s.span(),
9055 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9056 ));
9057 }
9058 child_fk_column = Some(trimmed.to_owned());
9059 return Ok(());
9060 }
9061 if inner.path.is_ident("self_pk_column") {
9062 let s: LitStr = inner.value()?.parse()?;
9063 let raw = s.value();
9064 let trimmed = raw.trim();
9065 if trimmed.is_empty() {
9066 return Err(syn::Error::new(
9067 s.span(),
9068 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9069 ));
9070 }
9071 self_pk_column = Some(trimmed.to_owned());
9072 return Ok(());
9073 }
9074 Err(inner.error(
9075 "unknown `reverse_has` attribute (supported: \
9076 `name`, `child`, `child_fk_column`, \
9077 `self_pk_column`)",
9078 ))
9079 })?;
9080 let Some(name) = accessor_name else {
9081 return Err(syn::Error::new(
9082 span,
9083 "`reverse_has` requires `name = \"...\"`",
9084 ));
9085 };
9086 let Some(child) = child_ident else {
9087 return Err(syn::Error::new(
9088 span,
9089 "`reverse_has` requires `child = \"ChildModelType\"`",
9090 ));
9091 };
9092 let Some(child_fk_column) = child_fk_column else {
9093 return Err(syn::Error::new(
9094 span,
9095 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9096 ));
9097 };
9098 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9099 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9100 return Err(syn::Error::new(
9101 span,
9102 format!(
9103 "duplicate `reverse_has(name = \"{name}\")` — \
9104 pick a unique accessor name"
9105 ),
9106 ));
9107 }
9108 out.reverse_has_relations.push(ReverseHasAttr {
9109 name,
9110 child,
9111 child_fk_column,
9112 self_pk_column,
9113 });
9114 return Ok(());
9115 }
9116 if meta.path.is_ident("generic_has") {
9117 // `#[rustango(generic_has(name = "tags",
9118 // child = "Tag", ct_column = "content_type_id",
9119 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9120 // — issue #830, the reverse generic-FK (GFK) arm of the
9121 // relation-existence family.
9122 let span = meta.path.span();
9123 let mut accessor_name: Option<String> = None;
9124 let mut child_ident: Option<syn::Ident> = None;
9125 let mut ct_column: Option<String> = None;
9126 let mut pk_column: Option<String> = None;
9127 let mut self_pk_column: Option<String> = None;
9128 meta.parse_nested_meta(|inner| {
9129 if inner.path.is_ident("name") {
9130 let s: LitStr = inner.value()?.parse()?;
9131 let raw = s.value();
9132 if raw.trim().is_empty() {
9133 return Err(syn::Error::new(
9134 s.span(),
9135 "`generic_has(name = \"...\")` must not be empty",
9136 ));
9137 }
9138 accessor_name = Some(raw);
9139 return Ok(());
9140 }
9141 if inner.path.is_ident("child") {
9142 let s: LitStr = inner.value()?.parse()?;
9143 let trimmed = s.value().trim().to_owned();
9144 if trimmed.is_empty() {
9145 return Err(syn::Error::new(
9146 s.span(),
9147 "`generic_has(child = \"...\")` must not be empty",
9148 ));
9149 }
9150 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9151 return Ok(());
9152 }
9153 if inner.path.is_ident("ct_column") {
9154 let s: LitStr = inner.value()?.parse()?;
9155 let trimmed = s.value().trim().to_owned();
9156 if trimmed.is_empty() {
9157 return Err(syn::Error::new(
9158 s.span(),
9159 "`generic_has(ct_column = \"...\")` must not be empty",
9160 ));
9161 }
9162 ct_column = Some(trimmed);
9163 return Ok(());
9164 }
9165 if inner.path.is_ident("pk_column") {
9166 let s: LitStr = inner.value()?.parse()?;
9167 let trimmed = s.value().trim().to_owned();
9168 if trimmed.is_empty() {
9169 return Err(syn::Error::new(
9170 s.span(),
9171 "`generic_has(pk_column = \"...\")` must not be empty",
9172 ));
9173 }
9174 pk_column = Some(trimmed);
9175 return Ok(());
9176 }
9177 if inner.path.is_ident("self_pk_column") {
9178 let s: LitStr = inner.value()?.parse()?;
9179 let trimmed = s.value().trim().to_owned();
9180 if trimmed.is_empty() {
9181 return Err(syn::Error::new(
9182 s.span(),
9183 "`generic_has(self_pk_column = \"...\")` must not be empty",
9184 ));
9185 }
9186 self_pk_column = Some(trimmed);
9187 return Ok(());
9188 }
9189 Err(inner.error(
9190 "unknown `generic_has` attribute (supported: \
9191 `name`, `child`, `ct_column`, `pk_column`, \
9192 `self_pk_column`)",
9193 ))
9194 })?;
9195 let Some(name) = accessor_name else {
9196 return Err(syn::Error::new(
9197 span,
9198 "`generic_has` requires `name = \"...\"`",
9199 ));
9200 };
9201 let Some(child) = child_ident else {
9202 return Err(syn::Error::new(
9203 span,
9204 "`generic_has` requires `child = \"ChildModelType\"`",
9205 ));
9206 };
9207 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9208 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9209 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9210 if out.generic_has_relations.iter().any(|r| r.name == name) {
9211 return Err(syn::Error::new(
9212 span,
9213 format!(
9214 "duplicate `generic_has(name = \"{name}\")` — \
9215 pick a unique accessor name"
9216 ),
9217 ));
9218 }
9219 out.generic_has_relations.push(GenericHasAttr {
9220 name,
9221 child,
9222 ct_column,
9223 pk_column,
9224 self_pk_column,
9225 });
9226 return Ok(());
9227 }
9228 if meta.path.is_ident("audit") {
9229 let mut audit = AuditAttrs::default();
9230 meta.parse_nested_meta(|inner| {
9231 if inner.path.is_ident("track") {
9232 let s: LitStr = inner.value()?.parse()?;
9233 audit.track =
9234 Some((split_field_list(&s.value()), s.span()));
9235 return Ok(());
9236 }
9237 Err(inner.error(
9238 "unknown audit attribute (supported: `track`)",
9239 ))
9240 })?;
9241 out.audit = Some(audit);
9242 return Ok(());
9243 }
9244 if meta.path.is_ident("permissions") {
9245 // Two forms accepted:
9246 // #[rustango(permissions)] — flag form, true
9247 // #[rustango(permissions = false)] — explicit opt-out
9248 // #[rustango(permissions = true)] — explicit opt-in
9249 if let Ok(v) = meta.value() {
9250 let lit: syn::LitBool = v.parse()?;
9251 out.permissions = lit.value;
9252 } else {
9253 out.permissions = true;
9254 }
9255 return Ok(());
9256 }
9257 if meta.path.is_ident("view") {
9258 // Issue #293 / T2.10. Two forms accepted, matching
9259 // the `permissions` flag pattern:
9260 // #[rustango(view)] — flag form, true
9261 // #[rustango(view = false)] — explicit opt-out
9262 // #[rustango(view = true)] — explicit opt-in
9263 if let Ok(v) = meta.value() {
9264 let lit: syn::LitBool = v.parse()?;
9265 out.is_view = lit.value;
9266 } else {
9267 out.is_view = true;
9268 }
9269 return Ok(());
9270 }
9271 if meta.path.is_ident("managed") {
9272 // Django-shape Meta.managed. Issue #321.
9273 // #[rustango(managed = false)] — operator-managed table
9274 // #[rustango(managed = true)] — rustango-managed (the default)
9275 // Bare-flag form is intentionally not accepted: writing
9276 // `#[rustango(managed)]` reads as "yes please manage it"
9277 // which is already the default. The opt-out is the only
9278 // useful state, so it must be explicit.
9279 let v = meta.value()?;
9280 let lit: syn::LitBool = v.parse()?;
9281 out.managed = lit.value;
9282 return Ok(());
9283 }
9284 if meta.path.is_ident("verbose_name") {
9285 let s: LitStr = meta.value()?.parse()?;
9286 out.verbose_name = Some(s.value());
9287 return Ok(());
9288 }
9289 if meta.path.is_ident("verbose_name_plural") {
9290 let s: LitStr = meta.value()?.parse()?;
9291 out.verbose_name_plural = Some(s.value());
9292 return Ok(());
9293 }
9294 if meta.path.is_ident("db_table_comment") {
9295 // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9296 // table-level comment attached to the DB catalog.
9297 let s: LitStr = meta.value()?.parse()?;
9298 out.db_table_comment = Some(s.value());
9299 return Ok(());
9300 }
9301 if meta.path.is_ident("proxy") {
9302 // Django-shape `Meta.proxy = True` — declarative flag
9303 // marking the struct as a proxy of another model that
9304 // shares its DB table. Stored on `ModelSchema::proxy`
9305 // so future codegen can skip `CreateTable` emission
9306 // for proxies (parent owns the table) and route
9307 // per-instance method resolution to the proxy class.
9308 //
9309 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9310 let value = if meta.input.peek(syn::Token![=]) {
9311 meta.value()?.parse::<syn::LitBool>()?.value
9312 } else {
9313 true
9314 };
9315 out.proxy = value;
9316 return Ok(());
9317 }
9318 if meta.path.is_ident("order_with_respect_to") {
9319 // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9320 // the model's instances are intrinsically ordered
9321 // relative to their parent FK. Django auto-generates
9322 // a `_order` integer column + admin reordering UI.
9323 //
9324 // rustango stores the FK field name on
9325 // `ModelSchema::order_with_respect_to`. Declarative-only
9326 // today: the migration writer + admin surfaces still
9327 // treat every model identically. Future codegen will
9328 // key off the metadata to auto-emit the `_order`
9329 // column and reorder helpers.
9330 //
9331 // Validated as a Rust-shape identifier so the macro
9332 // can reject typos at derive time.
9333 let s: LitStr = meta.value()?.parse()?;
9334 let raw = s.value();
9335 if raw.is_empty() {
9336 return Err(syn::Error::new(
9337 s.span(),
9338 "`order_with_respect_to` must be a non-empty FK field name",
9339 ));
9340 }
9341 let valid = raw
9342 .chars()
9343 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9344 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9345 if !valid {
9346 return Err(syn::Error::new(
9347 s.span(),
9348 format!(
9349 "`order_with_respect_to` must be a valid Rust \
9350 identifier (letters / digits / underscores, \
9351 not starting with a digit); got `{raw}`"
9352 ),
9353 ));
9354 }
9355 out.order_with_respect_to = Some(raw);
9356 return Ok(());
9357 }
9358 if meta.path.is_ident("required_db_features") {
9359 // Django-shape `Meta.required_db_features` — capability
9360 // tokens the model needs (e.g. `"json_extract"`,
9361 // `"window_functions"`, `"row_security"`). Comma-separated.
9362 // `manage check --deploy` walks every model and warns
9363 // when the active backend doesn't advertise the
9364 // capability.
9365 //
9366 // rustango ships a small registry of capability tokens
9367 // each dialect supports — see `Dialect::supports`.
9368 // Unknown tokens still parse (they end up on the
9369 // schema and show up in the warning) so projects can
9370 // declare aspirational capabilities and the check
9371 // verb will keep nagging until the dialect implements
9372 // them.
9373 let s: LitStr = meta.value()?.parse()?;
9374 let raw = s.value();
9375 let features: Vec<String> = raw
9376 .split(',')
9377 .map(str::trim)
9378 .filter(|s| !s.is_empty())
9379 .map(str::to_owned)
9380 .collect();
9381 if features.is_empty() {
9382 return Err(syn::Error::new(
9383 s.span(),
9384 "`required_db_features` must list at least one \
9385 comma-separated capability token",
9386 ));
9387 }
9388 out.required_db_features = features;
9389 return Ok(());
9390 }
9391 if meta.path.is_ident("required_db_vendor") {
9392 // Django-shape `Meta.required_db_vendor` — the model
9393 // is only meant to run against the named DB backend.
9394 // `manage check --deploy` flags a mismatch so
9395 // ops catches "I forgot to switch DATABASE_URL" at
9396 // deploy time rather than runtime.
9397 //
9398 // Django spells it as a free-form string; rustango
9399 // restricts to the three backends it ships dialects
9400 // for so the check verb can compare reliably.
9401 let s: LitStr = meta.value()?.parse()?;
9402 let raw = s.value().to_ascii_lowercase();
9403 match raw.as_str() {
9404 "postgresql" | "postgres" | "pg" => {
9405 out.required_db_vendor = Some("postgres".to_owned());
9406 }
9407 "mysql" | "mariadb" => {
9408 out.required_db_vendor = Some("mysql".to_owned());
9409 }
9410 "sqlite" | "sqlite3" => {
9411 out.required_db_vendor = Some("sqlite".to_owned());
9412 }
9413 _ => {
9414 return Err(syn::Error::new(
9415 s.span(),
9416 format!(
9417 "unknown required_db_vendor `{raw}` — \
9418 expected `postgres` (aliases: `postgresql`, `pg`), \
9419 `mysql` (alias: `mariadb`), or `sqlite` \
9420 (alias: `sqlite3`)"
9421 ),
9422 ));
9423 }
9424 }
9425 return Ok(());
9426 }
9427 if meta.path.is_ident("base_manager_name") {
9428 // Django-shape `Meta.base_manager_name` — name of the
9429 // Manager subclass that `<instance>.<relation>_set`
9430 // uses when resolving reverse-relation managers.
9431 // Distinct from `default_manager_name` (what
9432 // `Model.objects` returns at the class level).
9433 // Stored on `ModelSchema::base_manager_name`.
9434 //
9435 // Validated as a Rust identifier so it stays safe to
9436 // re-emit as code in future reverse-manager codegen.
9437 let s: LitStr = meta.value()?.parse()?;
9438 let raw = s.value();
9439 if raw.is_empty() {
9440 return Err(syn::Error::new(
9441 s.span(),
9442 "`base_manager_name` must be a non-empty string",
9443 ));
9444 }
9445 let valid = raw
9446 .chars()
9447 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9448 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9449 if !valid {
9450 return Err(syn::Error::new(
9451 s.span(),
9452 format!(
9453 "`base_manager_name` must be a valid Rust \
9454 identifier (letters / digits / underscores, \
9455 not starting with a digit); got `{raw}`"
9456 ),
9457 ));
9458 }
9459 out.base_manager_name = Some(raw);
9460 return Ok(());
9461 }
9462 if meta.path.is_ident("default_related_name") {
9463 // Django-shape `Meta.default_related_name` — the name
9464 // reverse-relation accessors use when callers don't
9465 // override `related_name=...` on the FK / M2M field.
9466 // Stored on `ModelSchema::default_related_name` so
9467 // future reverse-manager codegen / DRF schema emit /
9468 // admin templates can pick the right accessor name
9469 // (today rustango doesn't auto-emit reverse managers;
9470 // the metadata is the foundation for that work).
9471 //
9472 // Django requires snake_case + no `+` suffix; we
9473 // enforce non-empty + ASCII identifier-shape so the
9474 // string is safe to use as a Rust ident later.
9475 let s: LitStr = meta.value()?.parse()?;
9476 let raw = s.value();
9477 if raw.is_empty() {
9478 return Err(syn::Error::new(
9479 s.span(),
9480 "`default_related_name` must be a non-empty string",
9481 ));
9482 }
9483 let valid = raw
9484 .chars()
9485 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9486 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9487 if !valid {
9488 return Err(syn::Error::new(
9489 s.span(),
9490 format!(
9491 "`default_related_name` must be snake_case ASCII \
9492 (lowercase letters / digits / underscores, not \
9493 starting with a digit); got `{raw}`"
9494 ),
9495 ));
9496 }
9497 out.default_related_name = Some(raw);
9498 return Ok(());
9499 }
9500 if meta.path.is_ident("extra_permissions") {
9501 // Django-shape `Meta.permissions = [(codename, name), ...]`.
9502 // Comma-separated `codename:label` pairs.
9503 let s: LitStr = meta.value()?.parse()?;
9504 let raw = s.value();
9505 let mut pairs = Vec::new();
9506 for entry in raw.split(',') {
9507 let entry = entry.trim();
9508 if entry.is_empty() {
9509 continue;
9510 }
9511 let (codename, label) = match entry.split_once(':') {
9512 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9513 None => (entry.to_owned(), entry.to_owned()),
9514 };
9515 if codename.is_empty() {
9516 return Err(meta.error(
9517 "`extra_permissions` entries must be `codename:label` pairs",
9518 ));
9519 }
9520 pairs.push((codename, label));
9521 }
9522 if pairs.is_empty() {
9523 return Err(meta
9524 .error("`extra_permissions = \"…\"` must list at least one pair"));
9525 }
9526 out.extra_permissions = pairs;
9527 return Ok(());
9528 }
9529 if meta.path.is_ident("default_permissions") {
9530 // Django-shape `Meta.default_permissions = ('view',
9531 // 'change')`. Comma-separated subset of the CRUD
9532 // action set. Empty means all four (the framework
9533 // default — matches Django when the option is
9534 // omitted).
9535 let s: LitStr = meta.value()?.parse()?;
9536 let raw = s.value();
9537 let mut actions: Vec<String> = Vec::new();
9538 for entry in raw.split(',') {
9539 let action = entry.trim().to_ascii_lowercase();
9540 if action.is_empty() {
9541 continue;
9542 }
9543 match action.as_str() {
9544 "add" | "change" | "delete" | "view" => {}
9545 other => {
9546 return Err(syn::Error::new(
9547 s.span(),
9548 format!(
9549 "unknown default_permissions action `{other}` — \
9550 expected one of `add`, `change`, `delete`, `view`"
9551 ),
9552 ));
9553 }
9554 }
9555 if !actions.contains(&action) {
9556 actions.push(action);
9557 }
9558 }
9559 if actions.is_empty() {
9560 return Err(syn::Error::new(
9561 s.span(),
9562 "`default_permissions = \"…\"` must list at least one action; \
9563 use `permissions = false` on the container if you want NO \
9564 permissions seeded for this model.",
9565 ));
9566 }
9567 out.default_permissions = actions;
9568 return Ok(());
9569 }
9570 if meta.path.is_ident("get_latest_by") {
9571 // Django-shape `Meta.get_latest_by`. The `-` prefix
9572 // selects descending order (Django muscle memory).
9573 let s: LitStr = meta.value()?.parse()?;
9574 let raw = s.value();
9575 let trimmed = raw.trim();
9576 if trimmed.is_empty() {
9577 return Err(meta.error("`get_latest_by` must name a column"));
9578 }
9579 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9580 (stripped.to_owned(), true)
9581 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9582 (stripped.to_owned(), false)
9583 } else {
9584 (trimmed.to_owned(), false)
9585 };
9586 if col.is_empty() {
9587 return Err(meta.error("`get_latest_by` must name a column"));
9588 }
9589 out.get_latest_by = Some((col, desc));
9590 return Ok(());
9591 }
9592 if meta.path.is_ident("unique_together") {
9593 // Django-shape composite UNIQUE index. Two syntaxes:
9594 //
9595 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9596 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9597 //
9598 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9599 // (col1, col2)`, where <name> defaults to
9600 // `<table>_<col1>_<col2>_uq` when not supplied.
9601 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9602 out.indexes.push(IndexAttr {
9603 name,
9604 columns,
9605 unique: true,
9606 method: "btree".to_owned(),
9607 where_clause: None,
9608 include: Vec::new(),
9609 });
9610 return Ok(());
9611 }
9612 if meta.path.is_ident("index_together") {
9613 // Django-shape composite (non-unique) index. Two syntaxes
9614 // mirroring `unique_together`.
9615 //
9616 // #[rustango(index_together = "created_at, status")]
9617 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9618 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9619 out.indexes.push(IndexAttr {
9620 name,
9621 columns,
9622 unique: false,
9623 method: "btree".to_owned(),
9624 where_clause: None,
9625 include: Vec::new(),
9626 });
9627 return Ok(());
9628 }
9629 if meta.path.is_ident("unique_when") {
9630 // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9631 // partial unique index. Issue #265 / T1.3.
9632 //
9633 // #[rustango(unique_when(
9634 // columns = "email",
9635 // condition = "deleted_at IS NULL",
9636 // name = "unique_active_email"
9637 // ))]
9638 //
9639 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9640 // on PG / SQLite (both ship partial indexes natively).
9641 // MySQL falls back to a plain UNIQUE index — the
9642 // condition is lost; document the limitation in the
9643 // generated migration.
9644 let mut columns: Option<Vec<String>> = None;
9645 let mut condition: Option<String> = None;
9646 let mut name: Option<String> = None;
9647 let mut include: Vec<String> = Vec::new();
9648 meta.parse_nested_meta(|inner| {
9649 if inner.path.is_ident("columns") {
9650 let s: LitStr = inner.value()?.parse()?;
9651 columns = Some(split_field_list(&s.value()));
9652 return Ok(());
9653 }
9654 if inner.path.is_ident("condition") {
9655 let s: LitStr = inner.value()?.parse()?;
9656 condition = Some(s.value());
9657 return Ok(());
9658 }
9659 if inner.path.is_ident("name") {
9660 let s: LitStr = inner.value()?.parse()?;
9661 name = Some(s.value());
9662 return Ok(());
9663 }
9664 if inner.path.is_ident("include") {
9665 // Django `UniqueConstraint(include=[...])` — PG
9666 // 11+ covering-index columns. Non-key columns
9667 // travel with the index leaf for index-only
9668 // scans. Dropped on MySQL/SQLite by the writer.
9669 let s: LitStr = inner.value()?.parse()?;
9670 include = split_field_list(&s.value());
9671 return Ok(());
9672 }
9673 Err(inner.error(
9674 "unknown unique_when attribute (supported: \
9675 `columns = \"...\"`, `condition = \"...\"`, \
9676 `name = \"...\"`, `include = \"...\"`)",
9677 ))
9678 })?;
9679 let columns = columns.ok_or_else(|| {
9680 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9681 })?;
9682 let condition = condition.ok_or_else(|| {
9683 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9684 })?;
9685 if columns.is_empty() {
9686 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9687 }
9688 out.indexes.push(IndexAttr {
9689 name,
9690 columns,
9691 unique: true,
9692 method: "btree".to_owned(),
9693 where_clause: Some(condition),
9694 include,
9695 });
9696 return Ok(());
9697 }
9698 if meta.path.is_ident("index_when") {
9699 // Django `Index(fields=..., condition=Q(...))` parity —
9700 // non-unique partial index. Sibling of `unique_when`
9701 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9702 //
9703 // #[rustango(index_when(
9704 // columns = "status, created_at",
9705 // condition = "deleted_at IS NULL",
9706 // name = "active_status_created_idx"
9707 // ))]
9708 //
9709 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9710 // on PG / SQLite (both ship partial indexes natively).
9711 // MySQL has no native partial-index support — the writer
9712 // emits a plain CREATE INDEX and the condition is lost;
9713 // operators wanting that selectivity on MySQL should
9714 // declare a covering index plus an application-level
9715 // filter.
9716 let mut columns: Option<Vec<String>> = None;
9717 let mut condition: Option<String> = None;
9718 let mut name: Option<String> = None;
9719 let mut method: String = "btree".to_owned();
9720 let mut include: Vec<String> = Vec::new();
9721 meta.parse_nested_meta(|inner| {
9722 if inner.path.is_ident("columns") {
9723 let s: LitStr = inner.value()?.parse()?;
9724 columns = Some(split_field_list(&s.value()));
9725 return Ok(());
9726 }
9727 if inner.path.is_ident("condition") {
9728 let s: LitStr = inner.value()?.parse()?;
9729 condition = Some(s.value());
9730 return Ok(());
9731 }
9732 if inner.path.is_ident("name") {
9733 let s: LitStr = inner.value()?.parse()?;
9734 name = Some(s.value());
9735 return Ok(());
9736 }
9737 if inner.path.is_ident("method") {
9738 let s: LitStr = inner.value()?.parse()?;
9739 method = s.value();
9740 return Ok(());
9741 }
9742 if inner.path.is_ident("include") {
9743 // Django `Index(include=[...])` — PG 11+
9744 // covering-index columns; non-key columns
9745 // travel with the index leaf. Dropped on
9746 // MySQL/SQLite.
9747 let s: LitStr = inner.value()?.parse()?;
9748 include = split_field_list(&s.value());
9749 return Ok(());
9750 }
9751 Err(inner.error(
9752 "unknown index_when attribute (supported: \
9753 `columns = \"...\"`, `condition = \"...\"`, \
9754 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9755 `include = \"...\"`)",
9756 ))
9757 })?;
9758 let columns = columns
9759 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9760 let condition = condition.ok_or_else(|| {
9761 meta.error("`index_when(...)` requires `condition = \"...\"`")
9762 })?;
9763 if columns.is_empty() {
9764 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9765 }
9766 out.indexes.push(IndexAttr {
9767 name,
9768 columns,
9769 unique: false,
9770 method,
9771 where_clause: Some(condition),
9772 include,
9773 });
9774 return Ok(());
9775 }
9776 if meta.path.is_ident("index") {
9777 // Container-level composite index — legacy entry that
9778 // was advertised with a trailing `, unique, name = ...`
9779 // flag block which doesn't actually compose under
9780 // `parse_nested_meta`. Prefer `unique_together` /
9781 // `index_together` (above) for new code. The bare
9782 // `index = "..."` form is kept for back-compat: it
9783 // emits a non-unique composite index.
9784 let cols_lit: LitStr = meta.value()?.parse()?;
9785 let columns = split_field_list(&cols_lit.value());
9786 out.indexes.push(IndexAttr {
9787 name: None,
9788 columns,
9789 unique: false,
9790 method: "btree".to_owned(),
9791 where_clause: None,
9792 include: Vec::new(),
9793 });
9794 return Ok(());
9795 }
9796 if meta.path.is_ident("check") {
9797 // #[rustango(check(name = "…", expr = "…"))]
9798 let mut name: Option<String> = None;
9799 let mut expr: Option<String> = None;
9800 meta.parse_nested_meta(|inner| {
9801 if inner.path.is_ident("name") {
9802 let s: LitStr = inner.value()?.parse()?;
9803 name = Some(s.value());
9804 return Ok(());
9805 }
9806 if inner.path.is_ident("expr") {
9807 let s: LitStr = inner.value()?.parse()?;
9808 expr = Some(s.value());
9809 return Ok(());
9810 }
9811 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9812 })?;
9813 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9814 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9815 out.checks.push(CheckAttr { name, expr });
9816 return Ok(());
9817 }
9818 if meta.path.is_ident("exclude") {
9819 // #[rustango(exclude(name = "…", using = "gist",
9820 // elements = "col WITH op, col WITH op",
9821 // where = "…"))]
9822 let mut name: Option<String> = None;
9823 let mut using: Option<String> = None;
9824 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9825 let mut where_clause: Option<String> = None;
9826 meta.parse_nested_meta(|inner| {
9827 if inner.path.is_ident("name") {
9828 let s: LitStr = inner.value()?.parse()?;
9829 name = Some(s.value());
9830 return Ok(());
9831 }
9832 if inner.path.is_ident("using") {
9833 let s: LitStr = inner.value()?.parse()?;
9834 using = Some(s.value());
9835 return Ok(());
9836 }
9837 if inner.path.is_ident("elements") {
9838 let s: LitStr = inner.value()?.parse()?;
9839 elements_raw = Some((s.value(), s.span()));
9840 return Ok(());
9841 }
9842 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9843 let s: LitStr = inner.value()?.parse()?;
9844 where_clause = Some(s.value());
9845 return Ok(());
9846 }
9847 Err(inner.error(
9848 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9849 ))
9850 })?;
9851 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9852 let using = using.unwrap_or_else(|| "gist".to_owned());
9853 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9854 meta.error(
9855 "exclude requires `elements = \"col WITH op, col WITH op\"`",
9856 )
9857 })?;
9858 // Parse `col WITH op` pairs separated by commas.
9859 let mut elements: Vec<(String, String)> = Vec::new();
9860 for pair in elements_str.split(',') {
9861 let pair = pair.trim();
9862 if pair.is_empty() {
9863 continue;
9864 }
9865 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9866 let col = split.next().unwrap_or("").trim();
9867 let rest = split.next().unwrap_or("").trim();
9868 // `WITH op` — case-insensitive on `WITH`, then op.
9869 let rest_lc = rest.to_ascii_lowercase();
9870 let op = rest_lc
9871 .strip_prefix("with")
9872 .map(|r| r.trim_start())
9873 .filter(|r| !r.is_empty())
9874 .map(|_| {
9875 // Pull the original-case op from `rest` after the
9876 // `WITH ` token (5 chars).
9877 rest[4..].trim_start().to_owned()
9878 });
9879 let Some(op) = op else {
9880 return Err(syn::Error::new(
9881 elements_span,
9882 format!(
9883 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9884 (e.g. `room_id WITH =` or `during WITH &&`)"
9885 ),
9886 ));
9887 };
9888 if col.is_empty() || op.is_empty() {
9889 return Err(syn::Error::new(
9890 elements_span,
9891 format!(
9892 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9893 (both sides non-empty)"
9894 ),
9895 ));
9896 }
9897 elements.push((col.to_owned(), op));
9898 }
9899 if elements.is_empty() {
9900 return Err(syn::Error::new(
9901 elements_span,
9902 "exclude requires at least one `col WITH op` element",
9903 ));
9904 }
9905 out.excludes.push(ExcludeAttr {
9906 name,
9907 using,
9908 elements,
9909 where_clause,
9910 });
9911 return Ok(());
9912 }
9913 if meta.path.is_ident("generic_fk") {
9914 let mut gfk = GenericFkAttr {
9915 name: String::new(),
9916 ct_column: String::new(),
9917 pk_column: String::new(),
9918 };
9919 meta.parse_nested_meta(|inner| {
9920 if inner.path.is_ident("name") {
9921 let s: LitStr = inner.value()?.parse()?;
9922 gfk.name = s.value();
9923 return Ok(());
9924 }
9925 if inner.path.is_ident("ct_column") {
9926 let s: LitStr = inner.value()?.parse()?;
9927 gfk.ct_column = s.value();
9928 return Ok(());
9929 }
9930 if inner.path.is_ident("pk_column") {
9931 let s: LitStr = inner.value()?.parse()?;
9932 gfk.pk_column = s.value();
9933 return Ok(());
9934 }
9935 Err(inner.error(
9936 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
9937 ))
9938 })?;
9939 if gfk.name.is_empty() {
9940 return Err(meta.error("generic_fk requires `name = \"...\"`"));
9941 }
9942 if gfk.ct_column.is_empty() {
9943 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
9944 }
9945 if gfk.pk_column.is_empty() {
9946 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
9947 }
9948 out.generic_fks.push(gfk);
9949 return Ok(());
9950 }
9951 if meta.path.is_ident("fk_composite") {
9952 let mut fk = CompositeFkAttr {
9953 name: String::new(),
9954 to: String::new(),
9955 from: Vec::new(),
9956 on: Vec::new(),
9957 };
9958 meta.parse_nested_meta(|inner| {
9959 if inner.path.is_ident("name") {
9960 let s: LitStr = inner.value()?.parse()?;
9961 fk.name = s.value();
9962 return Ok(());
9963 }
9964 if inner.path.is_ident("to") {
9965 let s: LitStr = inner.value()?.parse()?;
9966 fk.to = s.value();
9967 return Ok(());
9968 }
9969 // `on = ("col1", "col2", ...)` — parse a parenthesised
9970 // comma-list of string literals.
9971 if inner.path.is_ident("on") || inner.path.is_ident("from") {
9972 let value = inner.value()?;
9973 let content;
9974 syn::parenthesized!(content in value);
9975 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
9976 content.parse_terminated(
9977 |p| p.parse::<syn::LitStr>(),
9978 syn::Token![,],
9979 )?;
9980 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
9981 if inner.path.is_ident("on") {
9982 fk.on = cols;
9983 } else {
9984 fk.from = cols;
9985 }
9986 return Ok(());
9987 }
9988 Err(inner.error(
9989 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
9990 ))
9991 })?;
9992 if fk.name.is_empty() {
9993 return Err(meta.error("fk_composite requires `name = \"...\"`"));
9994 }
9995 if fk.to.is_empty() {
9996 return Err(meta.error("fk_composite requires `to = \"...\"`"));
9997 }
9998 if fk.from.is_empty() || fk.on.is_empty() {
9999 return Err(meta.error(
10000 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10001 ));
10002 }
10003 if fk.from.len() != fk.on.len() {
10004 return Err(meta.error(format!(
10005 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10006 fk.from.len(),
10007 fk.on.len(),
10008 )));
10009 }
10010 out.composite_fks.push(fk);
10011 return Ok(());
10012 }
10013 if meta.path.is_ident("m2m") {
10014 let mut m2m = M2MAttr {
10015 name: String::new(),
10016 to: String::new(),
10017 through: String::new(),
10018 src: String::new(),
10019 dst: String::new(),
10020 auto_create: true,
10021 };
10022 meta.parse_nested_meta(|inner| {
10023 if inner.path.is_ident("name") {
10024 let s: LitStr = inner.value()?.parse()?;
10025 m2m.name = s.value();
10026 return Ok(());
10027 }
10028 if inner.path.is_ident("to") {
10029 let s: LitStr = inner.value()?.parse()?;
10030 m2m.to = s.value();
10031 return Ok(());
10032 }
10033 if inner.path.is_ident("through") {
10034 let s: LitStr = inner.value()?.parse()?;
10035 m2m.through = s.value();
10036 return Ok(());
10037 }
10038 if inner.path.is_ident("src") {
10039 let s: LitStr = inner.value()?.parse()?;
10040 m2m.src = s.value();
10041 return Ok(());
10042 }
10043 if inner.path.is_ident("dst") {
10044 let s: LitStr = inner.value()?.parse()?;
10045 m2m.dst = s.value();
10046 return Ok(());
10047 }
10048 if inner.path.is_ident("auto_create") {
10049 let lit: syn::LitBool = inner.value()?.parse()?;
10050 m2m.auto_create = lit.value;
10051 return Ok(());
10052 }
10053 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10054 })?;
10055 if m2m.name.is_empty() {
10056 return Err(meta.error("m2m requires `name = \"...\"`"));
10057 }
10058 if m2m.to.is_empty() {
10059 return Err(meta.error("m2m requires `to = \"...\"`"));
10060 }
10061 if m2m.through.is_empty() {
10062 return Err(meta.error("m2m requires `through = \"...\"`"));
10063 }
10064 if m2m.src.is_empty() {
10065 return Err(meta.error("m2m requires `src = \"...\"`"));
10066 }
10067 if m2m.dst.is_empty() {
10068 return Err(meta.error("m2m requires `dst = \"...\"`"));
10069 }
10070 out.m2m.push(m2m);
10071 return Ok(());
10072 }
10073 if meta.path.is_ident("generic_m2m") {
10074 let mut gm = GenericM2MAttr {
10075 name: String::new(),
10076 through: String::new(),
10077 pk_column: String::new(),
10078 ct_column: String::new(),
10079 related_column: String::new(),
10080 };
10081 meta.parse_nested_meta(|inner| {
10082 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10083 let s: LitStr = inner.value()?.parse()?;
10084 Ok(s.value())
10085 };
10086 if inner.path.is_ident("name") {
10087 gm.name = field(&inner)?;
10088 return Ok(());
10089 }
10090 if inner.path.is_ident("through") {
10091 gm.through = field(&inner)?;
10092 return Ok(());
10093 }
10094 if inner.path.is_ident("pk_column") {
10095 gm.pk_column = field(&inner)?;
10096 return Ok(());
10097 }
10098 if inner.path.is_ident("ct_column") {
10099 gm.ct_column = field(&inner)?;
10100 return Ok(());
10101 }
10102 if inner.path.is_ident("related_column") {
10103 gm.related_column = field(&inner)?;
10104 return Ok(());
10105 }
10106 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10107 })?;
10108 for (val, label) in [
10109 (&gm.name, "name"),
10110 (&gm.through, "through"),
10111 (&gm.pk_column, "pk_column"),
10112 (&gm.ct_column, "ct_column"),
10113 (&gm.related_column, "related_column"),
10114 ] {
10115 if val.is_empty() {
10116 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10117 }
10118 }
10119 out.generic_m2m.push(gm);
10120 return Ok(());
10121 }
10122 Err(meta.error("unknown rustango container attribute"))
10123 })?;
10124 }
10125 Ok(out)
10126}
10127
10128/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10129/// owned field names, trimming whitespace and skipping empty entries.
10130/// Field-name validation against the model is done by the caller.
10131fn split_field_list(raw: &str) -> Vec<String> {
10132 raw.split(',')
10133 .map(str::trim)
10134 .filter(|s| !s.is_empty())
10135 .map(str::to_owned)
10136 .collect()
10137}
10138
10139/// Shared parser for `unique_together` and `index_together` container
10140/// attrs. Accepts both shapes:
10141///
10142/// * `attr = "col1, col2"` — auto-derived index name.
10143/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10144///
10145/// Returns `(columns, name)`.
10146fn parse_together_attr(
10147 meta: &syn::meta::ParseNestedMeta<'_>,
10148 attr: &str,
10149) -> syn::Result<(Vec<String>, Option<String>)> {
10150 // Disambiguate by whether the next token is `=` (key-value) or
10151 // `(` (parenthesized).
10152 if meta.input.peek(syn::Token![=]) {
10153 let cols_lit: LitStr = meta.value()?.parse()?;
10154 let columns = split_field_list(&cols_lit.value());
10155 check_together_columns(meta, attr, &columns)?;
10156 return Ok((columns, None));
10157 }
10158 let mut columns: Option<Vec<String>> = None;
10159 let mut name: Option<String> = None;
10160 meta.parse_nested_meta(|inner| {
10161 if inner.path.is_ident("columns") {
10162 let s: LitStr = inner.value()?.parse()?;
10163 columns = Some(split_field_list(&s.value()));
10164 return Ok(());
10165 }
10166 if inner.path.is_ident("name") {
10167 let s: LitStr = inner.value()?.parse()?;
10168 name = Some(s.value());
10169 return Ok(());
10170 }
10171 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10172 })?;
10173 let columns = columns.ok_or_else(|| {
10174 meta.error(format!(
10175 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10176 ))
10177 })?;
10178 check_together_columns(meta, attr, &columns)?;
10179 Ok((columns, name))
10180}
10181
10182fn check_together_columns(
10183 meta: &syn::meta::ParseNestedMeta<'_>,
10184 attr: &str,
10185 columns: &[String],
10186) -> syn::Result<()> {
10187 if columns.len() < 2 {
10188 let single = if attr == "unique_together" {
10189 "#[rustango(unique)] on the field"
10190 } else {
10191 "#[rustango(index)] on the field"
10192 };
10193 return Err(meta.error(format!(
10194 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10195 )));
10196 }
10197 Ok(())
10198}
10199
10200/// Parse the fieldsets DSL: pipe-separated sections, optional
10201/// `"Title:"` prefix on each, comma-separated field names after.
10202/// Examples:
10203/// * `"name, office"` → one untitled section with two fields
10204/// * `"Identity: name, office | Metadata: created_at"` → two titled
10205/// sections
10206///
10207/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10208fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10209 raw.split('|')
10210 .map(str::trim)
10211 .filter(|s| !s.is_empty())
10212 .map(|section| {
10213 // Split off an optional `Title:` prefix (first colon).
10214 let (title, rest) = match section.split_once(':') {
10215 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10216 _ => (String::new(), section),
10217 };
10218 let fields = split_field_list(rest);
10219 (title, fields)
10220 })
10221 .collect()
10222}
10223
10224/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10225/// comma-separated entry maps a target field to one or more source
10226/// fields joined with `+`. Whitespace around tokens is trimmed.
10227/// Entries missing `:` or with empty target/source lists are dropped.
10228fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10229 raw.split(',')
10230 .map(str::trim)
10231 .filter(|s| !s.is_empty())
10232 .filter_map(|entry| {
10233 let (target, sources_raw) = entry.split_once(':')?;
10234 let target = target.trim().to_owned();
10235 if target.is_empty() {
10236 return None;
10237 }
10238 let sources: Vec<String> = sources_raw
10239 .split('+')
10240 .map(|s| s.trim().to_owned())
10241 .filter(|s| !s.is_empty())
10242 .collect();
10243 if sources.is_empty() {
10244 return None;
10245 }
10246 Some((target, sources))
10247 })
10248 .collect()
10249}
10250
10251/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10252/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10253/// and empty halves drop silently — the macro layer only enforces shape,
10254/// not field-name vs. widget-name validity (those checks happen at
10255/// `AdminConfig` consumption time). Issue #359.
10256fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10257 raw.split(',')
10258 .map(str::trim)
10259 .filter(|s| !s.is_empty())
10260 .filter_map(|entry| {
10261 let (field, widget) = entry.split_once(':')?;
10262 let field = field.trim().to_owned();
10263 let widget = widget.trim().to_owned();
10264 if field.is_empty() || widget.is_empty() {
10265 return None;
10266 }
10267 Some((field, widget))
10268 })
10269 .collect()
10270}
10271
10272/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10273/// Returns `(field_name, desc)` pairs in the same order as the input.
10274fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10275 raw.split(',')
10276 .map(str::trim)
10277 .filter(|s| !s.is_empty())
10278 .map(|spec| {
10279 spec.strip_prefix('-')
10280 .map_or((spec.to_owned(), false), |rest| {
10281 (rest.trim().to_owned(), true)
10282 })
10283 })
10284 .collect()
10285}
10286
10287struct FieldAttrs {
10288 column: Option<String>,
10289 primary_key: bool,
10290 fk: Option<String>,
10291 o2o: Option<String>,
10292 on: Option<String>,
10293 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10294 /// "set_default" | "no_action")]` — Django-shape
10295 /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10296 /// also set; the macro errors at compile time if applied to a
10297 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10298 /// DDL writer renders `ON DELETE <action>` after the constraint
10299 /// clause when this is `Some`; `None` falls back to the database
10300 /// default (NO ACTION on every backend rustango supports).
10301 on_delete: Option<String>,
10302 /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10303 /// reverse-accessor override. When set, the derive emits
10304 /// `Parent::<related_name>[_pool]` instead of the container-level
10305 /// `default_related_name` or the `<child_snake>_set[_pool]`
10306 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10307 /// silently ignored on non-FK fields. Follow-up to #816.
10308 related_name: Option<String>,
10309 max_length: Option<u32>,
10310 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10311 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10312 /// unconstrained `vector` column.
10313 vector_dims: Option<u32>,
10314 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10315 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10316 /// unconstrained `geometry(Point)` column (SRID 0).
10317 geometry_srid: Option<u32>,
10318 min: Option<i64>,
10319 max: Option<i64>,
10320 default: Option<String>,
10321 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10322 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10323 /// "gen_random_uuid()"`. The Rust field type must be
10324 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10325 /// INSERTs so the DB DEFAULT fires.
10326 auto_uuid: bool,
10327 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10328 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10329 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10330 /// the field is `Auto::Unset`, then bound as a normal parameter
10331 /// rather than relying on a per-dialect DB function. Issue #823
10332 /// (Eloquent `HasUuids`).
10333 ///
10334 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10335 /// Composes with every backend (PG / MySQL / SQLite) — no
10336 /// `gen_random_uuid()` requirement on the database.
10337 default_uuid_v7: bool,
10338 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10339 /// Server-set on insert, immutable from app code afterwards.
10340 /// Implies `auto + default = "now()"`. Field type must be
10341 /// `DateTime<Utc>`.
10342 auto_now_add: bool,
10343 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10344 /// every insert AND every update. Implies `auto + default =
10345 /// "now()"`; the macro additionally rewrites `update_on` /
10346 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10347 /// field value.
10348 auto_now: bool,
10349 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10350 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10351 /// `soft_delete_on(executor)` and `restore_on(executor)`
10352 /// methods on the model.
10353 soft_delete: bool,
10354 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10355 /// the column in the generated DDL.
10356 unique: bool,
10357 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10358 /// generates a `CREATE INDEX` for this column. `unique` here means
10359 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10360 index: bool,
10361 index_unique: bool,
10362 index_name: Option<String>,
10363 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10364 /// `"btree"`. Issue #34.
10365 index_method: String,
10366 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10367 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10368 /// the macro skips this column from every INSERT and UPDATE
10369 /// path, so the database always recomputes the value from
10370 /// `EXPR`. Backlog item #35.
10371 generated_as: Option<String>,
10372 /// `#[rustango(help_text = "…")]` — Django-shape help text
10373 /// rendered below the admin form's input. Threaded into
10374 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10375 /// layers can read it.
10376 help_text: Option<String>,
10377 /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10378 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10379 /// as a `&'static [(&'static str, &'static str)]` slice. When
10380 /// present, the admin form renders a `<select>` instead of `<input>`
10381 /// and the validator rejects values not in the list. Only meaningful
10382 /// for `FieldType::String`; the macro errors at compile time if
10383 /// applied to a non-string field.
10384 choices: Option<Vec<(String, String)>>,
10385 /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10386 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10387 /// the comment in CREATE TABLE; Postgres emits a separate
10388 /// `COMMENT ON COLUMN` statement after the table is created;
10389 /// SQLite silently drops the value (no native column comments).
10390 db_comment: Option<String>,
10391 /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10392 /// label for the field. Threaded into `FieldSchema::verbose_name`
10393 /// so admin column headers, form labels, and other display
10394 /// surfaces can prefer the friendly caption over the Rust
10395 /// identifier. `None` means renderers fall back to the field name.
10396 verbose_name: Option<String>,
10397 /// `#[rustango(editable = false)]` — Django-shape opt-out from
10398 /// auto-generated form rendering. Defaults to `true` so existing
10399 /// fields keep their current admin / form behavior; setting
10400 /// `false` removes the field from the admin change-form entirely
10401 /// (the value is still visible on detail / list views, just not
10402 /// editable).
10403 editable: bool,
10404 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10405 /// "form may submit empty even when DB is NOT NULL". Threaded into
10406 /// `FieldSchema::blank`. Defaults to `false`.
10407 blank: bool,
10408 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10409 /// Django-shape `CITextField`. Threaded into
10410 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10411 /// fields; the macro errors at derive time if applied elsewhere.
10412 case_insensitive: bool,
10413 /// `#[rustango(validators = "email,url")]` — Django-shape
10414 /// model-level validator chain. Comma-separated names that
10415 /// dispatch to the `validators::*` family in `validate_value`.
10416 /// Empty by default; fires on every typed INSERT/UPDATE.
10417 validators: Vec<String>,
10418}
10419
10420fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10421 let mut out = FieldAttrs {
10422 column: None,
10423 primary_key: false,
10424 fk: None,
10425 o2o: None,
10426 on: None,
10427 on_delete: None,
10428 related_name: None,
10429 max_length: None,
10430 vector_dims: None,
10431 geometry_srid: None,
10432 min: None,
10433 max: None,
10434 default: None,
10435 auto_uuid: false,
10436 default_uuid_v7: false,
10437 auto_now_add: false,
10438 auto_now: false,
10439 soft_delete: false,
10440 unique: false,
10441 index: false,
10442 index_unique: false,
10443 index_name: None,
10444 index_method: "btree".to_owned(),
10445 generated_as: None,
10446 help_text: None,
10447 choices: None,
10448 db_comment: None,
10449 verbose_name: None,
10450 editable: true,
10451 blank: false,
10452 case_insensitive: false,
10453 validators: Vec::new(),
10454 };
10455 for attr in &field.attrs {
10456 if !attr.path().is_ident("rustango") {
10457 continue;
10458 }
10459 attr.parse_nested_meta(|meta| {
10460 if meta.path.is_ident("column") {
10461 let s: LitStr = meta.value()?.parse()?;
10462 let name = s.value();
10463 validate_sql_identifier(&name, "column", s.span())?;
10464 out.column = Some(name);
10465 return Ok(());
10466 }
10467 if meta.path.is_ident("primary_key") {
10468 out.primary_key = true;
10469 return Ok(());
10470 }
10471 if meta.path.is_ident("fk") {
10472 let s: LitStr = meta.value()?.parse()?;
10473 out.fk = Some(s.value());
10474 return Ok(());
10475 }
10476 if meta.path.is_ident("o2o") {
10477 let s: LitStr = meta.value()?.parse()?;
10478 out.o2o = Some(s.value());
10479 return Ok(());
10480 }
10481 if meta.path.is_ident("on") {
10482 let s: LitStr = meta.value()?.parse()?;
10483 out.on = Some(s.value());
10484 return Ok(());
10485 }
10486 if meta.path.is_ident("on_delete") {
10487 let s: LitStr = meta.value()?.parse()?;
10488 let raw = s.value();
10489 let normalized = raw.trim().to_ascii_lowercase();
10490 // Validate at parse time so the user gets a clear span
10491 // rather than a downstream compile error in the emit.
10492 match normalized.as_str() {
10493 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10494 _ => {
10495 return Err(syn::Error::new(
10496 s.span(),
10497 format!(
10498 "unknown on_delete action `{raw}`; expected one of \
10499 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10500 ),
10501 ));
10502 }
10503 }
10504 out.on_delete = Some(normalized);
10505 return Ok(());
10506 }
10507 if meta.path.is_ident("related_name") {
10508 let s: LitStr = meta.value()?.parse()?;
10509 let raw = s.value();
10510 if raw.trim().is_empty() {
10511 return Err(syn::Error::new(
10512 s.span(),
10513 "`related_name` must be a non-empty identifier",
10514 ));
10515 }
10516 // Validate as a Rust-ident shape — the value becomes
10517 // a method name on the parent type. Same rule as
10518 // `default_related_name` so the two surfaces match.
10519 if !raw
10520 .chars()
10521 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10522 || raw.starts_with(char::is_numeric)
10523 {
10524 return Err(syn::Error::new(
10525 s.span(),
10526 "`related_name` must be snake_case ASCII (lowercase letters, \
10527 digits, underscores; no leading digit)",
10528 ));
10529 }
10530 out.related_name = Some(raw);
10531 return Ok(());
10532 }
10533 if meta.path.is_ident("max_length") {
10534 let lit: syn::LitInt = meta.value()?.parse()?;
10535 out.max_length = Some(lit.base10_parse::<u32>()?);
10536 return Ok(());
10537 }
10538 // `#[rustango(vector(dims = N))]` — pgvector column
10539 // dimension (#824). Nested-meta form so it reads like the
10540 // other typed-column attrs.
10541 if meta.path.is_ident("vector") {
10542 meta.parse_nested_meta(|inner| {
10543 if inner.path.is_ident("dims") {
10544 let lit: syn::LitInt = inner.value()?.parse()?;
10545 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10546 return Ok(());
10547 }
10548 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10549 })?;
10550 return Ok(());
10551 }
10552 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10553 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10554 if meta.path.is_ident("geometry") {
10555 meta.parse_nested_meta(|inner| {
10556 if inner.path.is_ident("srid") {
10557 let lit: syn::LitInt = inner.value()?.parse()?;
10558 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10559 return Ok(());
10560 }
10561 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10562 })?;
10563 return Ok(());
10564 }
10565 if meta.path.is_ident("min") {
10566 out.min = Some(parse_signed_i64(&meta)?);
10567 return Ok(());
10568 }
10569 if meta.path.is_ident("max") {
10570 out.max = Some(parse_signed_i64(&meta)?);
10571 return Ok(());
10572 }
10573 if meta.path.is_ident("default") {
10574 let s: LitStr = meta.value()?.parse()?;
10575 out.default = Some(s.value());
10576 return Ok(());
10577 }
10578 if meta.path.is_ident("generated_as") {
10579 let s: LitStr = meta.value()?.parse()?;
10580 out.generated_as = Some(s.value());
10581 return Ok(());
10582 }
10583 if meta.path.is_ident("help_text") {
10584 let s: LitStr = meta.value()?.parse()?;
10585 out.help_text = Some(s.value());
10586 return Ok(());
10587 }
10588 if meta.path.is_ident("choices") {
10589 let s: LitStr = meta.value()?.parse()?;
10590 let raw = s.value();
10591 let mut pairs: Vec<(String, String)> = Vec::new();
10592 for chunk in raw.split(',') {
10593 let chunk = chunk.trim();
10594 if chunk.is_empty() {
10595 continue;
10596 }
10597 let (value, label) = match chunk.split_once(':') {
10598 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10599 None => (chunk.to_owned(), chunk.to_owned()),
10600 };
10601 if value.is_empty() {
10602 return Err(syn::Error::new(
10603 s.span(),
10604 "`choices` entry has empty value before `:`",
10605 ));
10606 }
10607 pairs.push((value, label));
10608 }
10609 if pairs.is_empty() {
10610 return Err(syn::Error::new(
10611 s.span(),
10612 "`choices = \"…\"` must contain at least one value",
10613 ));
10614 }
10615 out.choices = Some(pairs);
10616 return Ok(());
10617 }
10618 if meta.path.is_ident("db_comment") {
10619 let s: LitStr = meta.value()?.parse()?;
10620 out.db_comment = Some(s.value());
10621 return Ok(());
10622 }
10623 if meta.path.is_ident("verbose_name") {
10624 let s: LitStr = meta.value()?.parse()?;
10625 out.verbose_name = Some(s.value());
10626 return Ok(());
10627 }
10628 if meta.path.is_ident("editable") {
10629 // Two forms accepted:
10630 // #[rustango(editable = false)] / true — explicit
10631 // #[rustango(editable)] — flag form (= true, the
10632 // default, so harmless; included for symmetry)
10633 if let Ok(v) = meta.value() {
10634 let lit: syn::LitBool = v.parse()?;
10635 out.editable = lit.value;
10636 } else {
10637 out.editable = true;
10638 }
10639 return Ok(());
10640 }
10641 if meta.path.is_ident("blank") {
10642 // Two forms accepted:
10643 // #[rustango(blank)] — flag form, true
10644 // #[rustango(blank = false)] / true — explicit
10645 if let Ok(v) = meta.value() {
10646 let lit: syn::LitBool = v.parse()?;
10647 out.blank = lit.value;
10648 } else {
10649 out.blank = true;
10650 }
10651 return Ok(());
10652 }
10653 if meta.path.is_ident("citext") {
10654 // Django-parity CITextField (#344). Two forms:
10655 // #[rustango(citext)] — flag form, true
10656 // #[rustango(citext = true)] — explicit
10657 // #[rustango(citext = false)] — explicit opt-out
10658 // String-only validation lives in the field-type
10659 // emitter (the FieldType discriminant is computed in
10660 // `detect_type`); the macro records the flag and the
10661 // DDL writer emits dialect-specific COLLATE / CITEXT.
10662 if let Ok(v) = meta.value() {
10663 let lit: syn::LitBool = v.parse()?;
10664 out.case_insensitive = lit.value;
10665 } else {
10666 out.case_insensitive = true;
10667 }
10668 return Ok(());
10669 }
10670 if meta.path.is_ident("validators") {
10671 let s: LitStr = meta.value()?.parse()?;
10672 let raw = s.value();
10673 out.validators = raw
10674 .split(',')
10675 .map(str::trim)
10676 .filter(|s| !s.is_empty())
10677 .map(str::to_owned)
10678 .collect();
10679 if out.validators.is_empty() {
10680 return Err(syn::Error::new(
10681 s.span(),
10682 "`validators = \"…\"` must list at least one name",
10683 ));
10684 }
10685 return Ok(());
10686 }
10687 if meta.path.is_ident("auto_uuid") {
10688 out.auto_uuid = true;
10689 // Implied: PK + auto + DEFAULT gen_random_uuid().
10690 // Each is also explicitly settable; the explicit
10691 // value wins if conflicting.
10692 out.primary_key = true;
10693 if out.default.is_none() {
10694 out.default = Some("gen_random_uuid()".into());
10695 }
10696 return Ok(());
10697 }
10698 if meta.path.is_ident("default_uuid_v7") {
10699 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10700 // No SQL DEFAULT (the macro fills the value Rust-side
10701 // before binding); just mark the field as PK + Auto
10702 // so the insert path is the `Auto::Unset → generate`
10703 // branch.
10704 out.default_uuid_v7 = true;
10705 out.primary_key = true;
10706 return Ok(());
10707 }
10708 if meta.path.is_ident("auto_now_add") {
10709 out.auto_now_add = true;
10710 if out.default.is_none() {
10711 out.default = Some("now()".into());
10712 }
10713 return Ok(());
10714 }
10715 if meta.path.is_ident("auto_now") {
10716 out.auto_now = true;
10717 if out.default.is_none() {
10718 out.default = Some("now()".into());
10719 }
10720 return Ok(());
10721 }
10722 if meta.path.is_ident("soft_delete") {
10723 out.soft_delete = true;
10724 return Ok(());
10725 }
10726 if meta.path.is_ident("unique") {
10727 out.unique = true;
10728 return Ok(());
10729 }
10730 if meta.path.is_ident("index") {
10731 out.index = true;
10732 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10733 if meta.input.peek(syn::token::Paren) {
10734 meta.parse_nested_meta(|inner| {
10735 if inner.path.is_ident("unique") {
10736 out.index_unique = true;
10737 return Ok(());
10738 }
10739 if inner.path.is_ident("name") {
10740 let s: LitStr = inner.value()?.parse()?;
10741 out.index_name = Some(s.value());
10742 return Ok(());
10743 }
10744 if inner.path.is_ident("method") {
10745 let s: LitStr = inner.value()?.parse()?;
10746 let v = s.value();
10747 match v.as_str() {
10748 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10749 out.index_method = v;
10750 }
10751 other => {
10752 return Err(inner.error(format!(
10753 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10754 )));
10755 }
10756 }
10757 return Ok(());
10758 }
10759 Err(inner.error(
10760 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10761 ))
10762 })?;
10763 }
10764 return Ok(());
10765 }
10766 Err(meta.error("unknown rustango field attribute"))
10767 })?;
10768 }
10769 Ok(out)
10770}
10771
10772/// Parse a signed integer literal, accepting optional leading `-`.
10773fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10774 let expr: syn::Expr = meta.value()?.parse()?;
10775 match expr {
10776 syn::Expr::Lit(syn::ExprLit {
10777 lit: syn::Lit::Int(lit),
10778 ..
10779 }) => lit.base10_parse::<i64>(),
10780 syn::Expr::Unary(syn::ExprUnary {
10781 op: syn::UnOp::Neg(_),
10782 expr,
10783 ..
10784 }) => {
10785 if let syn::Expr::Lit(syn::ExprLit {
10786 lit: syn::Lit::Int(lit),
10787 ..
10788 }) = *expr
10789 {
10790 let v: i64 = lit.base10_parse()?;
10791 Ok(-v)
10792 } else {
10793 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10794 }
10795 }
10796 other => Err(syn::Error::new_spanned(
10797 other,
10798 "expected integer literal (signed)",
10799 )),
10800 }
10801}
10802
10803struct FieldInfo<'a> {
10804 ident: &'a syn::Ident,
10805 column: String,
10806 primary_key: bool,
10807 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10808 /// skip this column when `Auto::Unset` and emit it under
10809 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10810 auto: bool,
10811 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10812 /// the `Column::Value` associated type for typed-column tokens.
10813 value_ty: &'a Type,
10814 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10815 field_type_tokens: TokenStream2,
10816 schema: TokenStream2,
10817 from_row_init: TokenStream2,
10818 /// Variant of [`Self::from_row_init`] that reads the column via
10819 /// `format!("{prefix}__{col}")` so a model can be decoded out of
10820 /// the aliased columns of a JOINed row. Drives slice 9.0d's
10821 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10822 /// helper that `select_related` calls when stitching loaded FKs.
10823 from_aliased_row_init: TokenStream2,
10824 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10825 /// relation helper emit (`Author::<child>_set`) needs to know `T`
10826 /// to point the generated method at the right child model.
10827 fk_inner: Option<Type>,
10828 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10829 /// `kind` (since ForeignKey detection sets `kind` to K's
10830 /// underlying type) but stored separately for clarity at the
10831 /// `FkRelation` construction site, which only sees the FK's
10832 /// surface fields.
10833 fk_pk_kind: DetectedKind,
10834 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10835 /// the bare `ForeignKey<T, K>`. Routes the load_related and
10836 /// fk_pk_access emitters to wrap assignments / accessors in
10837 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10838 /// FK column compiles end-to-end. The DDL writer reads this off
10839 /// the field schema (`nullable` flag); the macro just needs to
10840 /// keep the Rust-side codegen consistent.
10841 nullable: bool,
10842 /// `true` when this column was marked `#[rustango(auto_now)]` —
10843 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10844 /// column instead of the user-supplied value, so `updated_at`
10845 /// always reflects the latest write without the caller having
10846 /// to remember to set it.
10847 auto_now: bool,
10848 /// `true` when this column was marked `#[rustango(auto_now_add)]`
10849 /// — the column is server-set on INSERT (DB DEFAULT) and
10850 /// **immutable** afterwards. `update_on` / `save_on` skip the
10851 /// column entirely so a stale `created_at` value in memory never
10852 /// rewrites the persisted timestamp.
10853 auto_now_add: bool,
10854 /// `true` when this column was marked `#[rustango(soft_delete)]`.
10855 /// Triggers emission of `soft_delete_on(executor)` and
10856 /// `restore_on(executor)` on the model's inherent impl. There is
10857 /// at most one such column per model — emission asserts this.
10858 soft_delete: bool,
10859 /// `Some` when this column was marked
10860 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10861 /// every INSERT and UPDATE path; the database recomputes the
10862 /// value from `EXPR`. Backlog item #35.
10863 generated_as: Option<String>,
10864 /// `true` when this column was marked
10865 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10866 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10867 /// with `Uuid::now_v7()` before binding, so the PK is generated
10868 /// Rust-side and the column is always present in the INSERT
10869 /// statement (no DB DEFAULT requirement). Issue #823.
10870 default_uuid_v7: bool,
10871 /// `Some` when this FK field carried `#[rustango(related_name =
10872 /// "...")]`. Threaded into [`FkRelation::related_name`] and
10873 /// consumed by [`reverse_helper_tokens`] to override the default
10874 /// `<child_snake>_set` accessor name. Follow-up to #816.
10875 related_name: Option<String>,
10876}
10877
10878/// Reject table names that won't survive SQL identifier
10879/// derivation downstream. Postgres' regular-identifier rule
10880/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10881/// through the framework's unquoted FK / index / constraint name
10882/// emission without surprises. We also disallow leading-digit and
10883/// the empty string for clarity.
10884///
10885/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10886/// here — those produce a runtime error from the SQL parser,
10887/// which is loud enough; statically enumerating reserved words
10888/// across the three supported dialects is more friction than help.
10889///
10890/// Backlog item #65.
10891fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
10892 validate_sql_identifier(name, "table", span)
10893}
10894
10895/// Reject SQL identifiers that compile but break downstream SQL
10896/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
10897/// `kind` is "table" / "column" — used for the error message so users
10898/// see which attribute caused the failure.
10899fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
10900 if name.is_empty() {
10901 return Err(syn::Error::new(
10902 span,
10903 format!("`{kind} = \"\"` is not a valid SQL identifier"),
10904 ));
10905 }
10906 let mut chars = name.chars();
10907 let first = chars.next().unwrap();
10908 if !(first.is_ascii_alphabetic() || first == '_') {
10909 return Err(syn::Error::new(
10910 span,
10911 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
10912 ));
10913 }
10914 for c in chars {
10915 if !(c.is_ascii_alphanumeric() || c == '_') {
10916 return Err(syn::Error::new(
10917 span,
10918 format!(
10919 "{kind} name `{name}` contains invalid character {c:?} — \
10920 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
10921 Hyphens in particular break FK / index name derivation \
10922 downstream; use underscores instead (e.g. `{}`)",
10923 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
10924 ),
10925 ));
10926 }
10927 }
10928 Ok(())
10929}
10930
10931fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
10932 let root = rustango_root();
10933 let attrs = parse_field_attrs(field)?;
10934 let ident = field
10935 .ident
10936 .as_ref()
10937 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
10938 let name = ident.to_string();
10939 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
10940 let primary_key = attrs.primary_key;
10941 let DetectedType {
10942 kind,
10943 nullable,
10944 auto: detected_auto,
10945 fk_inner,
10946 } = detect_type(&field.ty)?;
10947 check_bound_compatibility(field, &attrs, kind)?;
10948 let auto = detected_auto;
10949 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
10950 // INSERT path: the user must wrap the field in `Auto<T>`, which
10951 // marks the column as DB-default-supplied. The mixin attrs then
10952 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
10953 // for `auto_now`, force the value on UPDATE too.
10954 if attrs.auto_uuid {
10955 if kind != DetectedKind::Uuid {
10956 return Err(syn::Error::new_spanned(
10957 field,
10958 "`#[rustango(auto_uuid)]` requires the field type to be \
10959 `Auto<uuid::Uuid>`",
10960 ));
10961 }
10962 if !detected_auto {
10963 return Err(syn::Error::new_spanned(
10964 field,
10965 "`#[rustango(auto_uuid)]` requires the field type to be \
10966 wrapped in `Auto<...>` so the macro skips the column on \
10967 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
10968 ));
10969 }
10970 }
10971 if attrs.default_uuid_v7 {
10972 if kind != DetectedKind::Uuid {
10973 return Err(syn::Error::new_spanned(
10974 field,
10975 "`#[rustango(default_uuid_v7)]` requires the field type to be \
10976 `Auto<uuid::Uuid>`",
10977 ));
10978 }
10979 if !detected_auto {
10980 return Err(syn::Error::new_spanned(
10981 field,
10982 "`#[rustango(default_uuid_v7)]` requires the field type to be \
10983 wrapped in `Auto<...>` so the macro can detect the \
10984 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
10985 ));
10986 }
10987 if attrs.auto_uuid {
10988 return Err(syn::Error::new_spanned(
10989 field,
10990 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
10991 `#[rustango(auto_uuid)]` — the former generates the UUID \
10992 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
10993 Pick one.",
10994 ));
10995 }
10996 }
10997 if attrs.auto_now_add || attrs.auto_now {
10998 if kind != DetectedKind::DateTime {
10999 return Err(syn::Error::new_spanned(
11000 field,
11001 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11002 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11003 ));
11004 }
11005 if !detected_auto {
11006 return Err(syn::Error::new_spanned(
11007 field,
11008 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11009 the field type to be wrapped in `Auto<...>` so the macro skips \
11010 the column on INSERT and the DB DEFAULT (`now()`) fires",
11011 ));
11012 }
11013 }
11014 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11015 return Err(syn::Error::new_spanned(
11016 field,
11017 "`#[rustango(soft_delete)]` requires the field type to be \
11018 `Option<chrono::DateTime<chrono::Utc>>`",
11019 ));
11020 }
11021 let is_mixin_auto =
11022 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11023 if detected_auto && !primary_key && !is_mixin_auto {
11024 return Err(syn::Error::new_spanned(
11025 field,
11026 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11027 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11028 `auto_now`",
11029 ));
11030 }
11031 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11032 return Err(syn::Error::new_spanned(
11033 field,
11034 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11035 SERIAL / BIGSERIAL already supplies a default sequence.",
11036 ));
11037 }
11038 if fk_inner.is_some() && primary_key {
11039 return Err(syn::Error::new_spanned(
11040 field,
11041 "`ForeignKey<T>` is not allowed on a primary-key field — \
11042 a row's PK is its own identity, not a reference to a parent.",
11043 ));
11044 }
11045 if attrs.generated_as.is_some() {
11046 if primary_key {
11047 return Err(syn::Error::new_spanned(
11048 field,
11049 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11050 primary-key field — a PK must be writable so the row \
11051 has an identity at INSERT time.",
11052 ));
11053 }
11054 if attrs.default.is_some() {
11055 return Err(syn::Error::new_spanned(
11056 field,
11057 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11058 `default = \"…\"` — Postgres rejects DEFAULT on \
11059 generated columns. The expression IS the default.",
11060 ));
11061 }
11062 if detected_auto {
11063 return Err(syn::Error::new_spanned(
11064 field,
11065 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11066 an `Auto<T>` field — generated columns are computed \
11067 by the DB, not server-assigned via a sequence. Use a \
11068 plain Rust type (e.g. `f64`).",
11069 ));
11070 }
11071 if fk_inner.is_some() {
11072 return Err(syn::Error::new_spanned(
11073 field,
11074 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11075 ForeignKey field.",
11076 ));
11077 }
11078 }
11079 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11080 let column_lit = column.as_str();
11081 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11082 // dimension that the bare `Vector` Rust type can't carry, so emit
11083 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11084 // fallback of `Vector(0)`.
11085 let field_type_tokens = if kind == DetectedKind::Vector {
11086 let root = rustango_root();
11087 let dims = attrs.vector_dims.unwrap_or(0);
11088 quote!(#root::core::FieldType::Vector(#dims))
11089 } else if kind == DetectedKind::Geometry {
11090 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11091 // SRID that the bare `Point` Rust type can't carry, so emit
11092 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11093 let root = rustango_root();
11094 let srid = attrs.geometry_srid.unwrap_or(0);
11095 quote!(#root::core::FieldType::Geometry(#srid))
11096 } else {
11097 kind.variant_tokens()
11098 };
11099 let max_length = optional_u32(attrs.max_length);
11100 let min = optional_i64(attrs.min);
11101 let max = optional_i64(attrs.max);
11102 let default = optional_str(attrs.default.as_deref());
11103
11104 let unique = attrs.unique;
11105 let generated_as = optional_str(attrs.generated_as.as_deref());
11106 let help_text = optional_str(attrs.help_text.as_deref());
11107 let choices = optional_choices(attrs.choices.as_deref());
11108 let db_comment = optional_str(attrs.db_comment.as_deref());
11109 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11110 let editable = attrs.editable;
11111 let blank = attrs.blank;
11112 let case_insensitive = attrs.case_insensitive;
11113 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11114 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11115 return Err(syn::Error::new_spanned(
11116 field,
11117 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11118 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11119 non-FK column.",
11120 ));
11121 }
11122 let fk_on_delete = match attrs.on_delete.as_deref() {
11123 None => quote!(::core::option::Option::None),
11124 Some(action) => {
11125 let variant = match action {
11126 "cascade" => quote!(Cascade),
11127 "restrict" => quote!(Restrict),
11128 "set_null" => quote!(SetNull),
11129 "set_default" => quote!(SetDefault),
11130 "no_action" => quote!(NoAction),
11131 // parse_field_attrs already validated this — guard against future drift.
11132 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11133 };
11134 quote!(::core::option::Option::Some(
11135 #root::core::OnDeleteAction::#variant
11136 ))
11137 }
11138 };
11139 let schema = quote! {
11140 #root::core::FieldSchema {
11141 name: #name,
11142 column: #column_lit,
11143 ty: #field_type_tokens,
11144 nullable: #nullable,
11145 primary_key: #primary_key,
11146 relation: #relation,
11147 max_length: #max_length,
11148 min: #min,
11149 max: #max,
11150 default: #default,
11151 auto: #auto,
11152 unique: #unique,
11153 generated_as: #generated_as,
11154 help_text: #help_text,
11155 choices: #choices,
11156 db_comment: #db_comment,
11157 verbose_name: #verbose_name,
11158 editable: #editable,
11159 blank: #blank,
11160 case_insensitive: #case_insensitive,
11161 fk_on_delete: #fk_on_delete,
11162 validators: &[ #(#validators_lits),* ],
11163 }
11164 };
11165
11166 let from_row_init = quote! {
11167 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11168 };
11169 let from_aliased_row_init = quote! {
11170 #ident: #root::sql::sqlx::Row::try_get(
11171 row,
11172 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11173 )?
11174 };
11175
11176 Ok(FieldInfo {
11177 ident,
11178 column,
11179 primary_key,
11180 auto,
11181 value_ty: &field.ty,
11182 field_type_tokens,
11183 schema,
11184 from_row_init,
11185 from_aliased_row_init,
11186 fk_inner: fk_inner.cloned(),
11187 fk_pk_kind: kind,
11188 nullable,
11189 auto_now: attrs.auto_now,
11190 auto_now_add: attrs.auto_now_add,
11191 soft_delete: attrs.soft_delete,
11192 generated_as: attrs.generated_as.clone(),
11193 default_uuid_v7: attrs.default_uuid_v7,
11194 related_name: attrs.related_name.clone(),
11195 })
11196}
11197
11198fn check_bound_compatibility(
11199 field: &syn::Field,
11200 attrs: &FieldAttrs,
11201 kind: DetectedKind,
11202) -> syn::Result<()> {
11203 if attrs.max_length.is_some() && kind != DetectedKind::String {
11204 return Err(syn::Error::new_spanned(
11205 field,
11206 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11207 ));
11208 }
11209 if attrs.choices.is_some() && kind != DetectedKind::String {
11210 return Err(syn::Error::new_spanned(
11211 field,
11212 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11213 integer-valued enumerations should be modeled with a Rust enum and \
11214 custom (de)serializer for now",
11215 ));
11216 }
11217 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11218 return Err(syn::Error::new_spanned(
11219 field,
11220 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11221 ));
11222 }
11223 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11224 if min > max {
11225 return Err(syn::Error::new_spanned(
11226 field,
11227 format!("`min` ({min}) is greater than `max` ({max})"),
11228 ));
11229 }
11230 }
11231 Ok(())
11232}
11233
11234fn optional_u32(value: Option<u32>) -> TokenStream2 {
11235 if let Some(v) = value {
11236 quote!(::core::option::Option::Some(#v))
11237 } else {
11238 quote!(::core::option::Option::None)
11239 }
11240}
11241
11242fn optional_i64(value: Option<i64>) -> TokenStream2 {
11243 if let Some(v) = value {
11244 quote!(::core::option::Option::Some(#v))
11245 } else {
11246 quote!(::core::option::Option::None)
11247 }
11248}
11249
11250fn optional_str(value: Option<&str>) -> TokenStream2 {
11251 if let Some(v) = value {
11252 quote!(::core::option::Option::Some(#v))
11253 } else {
11254 quote!(::core::option::Option::None)
11255 }
11256}
11257
11258fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11259 let Some(pairs) = pairs else {
11260 return quote!(::core::option::Option::None);
11261 };
11262 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11263 quote!(::core::option::Option::Some(&[#(#entries),*]))
11264}
11265
11266fn relation_tokens(
11267 field: &syn::Field,
11268 attrs: &FieldAttrs,
11269 fk_inner: Option<&syn::Type>,
11270 table: &str,
11271) -> syn::Result<TokenStream2> {
11272 let root = rustango_root();
11273 if let Some(inner) = fk_inner {
11274 if attrs.fk.is_some() || attrs.o2o.is_some() {
11275 return Err(syn::Error::new_spanned(
11276 field,
11277 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11278 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11279 ));
11280 }
11281 let on = attrs.on.as_deref().unwrap_or("id");
11282 return Ok(quote! {
11283 ::core::option::Option::Some(#root::core::Relation::Fk {
11284 to: <#inner as #root::core::Model>::SCHEMA.table,
11285 on: #on,
11286 })
11287 });
11288 }
11289 match (&attrs.fk, &attrs.o2o) {
11290 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11291 field,
11292 "`fk` and `o2o` are mutually exclusive",
11293 )),
11294 (Some(to), None) => {
11295 let on = attrs.on.as_deref().unwrap_or("id");
11296 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11297 // the model's own table. Threaded as a literal string at
11298 // macro-expansion time to sidestep the const-eval cycle
11299 // that `Self::SCHEMA.table` would create when referenced
11300 // inside Self::SCHEMA's own initializer.
11301 let resolved = if to == "self" { table } else { to };
11302 Ok(quote! {
11303 ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11304 })
11305 }
11306 (None, Some(to)) => {
11307 let on = attrs.on.as_deref().unwrap_or("id");
11308 let resolved = if to == "self" { table } else { to };
11309 Ok(quote! {
11310 ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11311 })
11312 }
11313 (None, None) => {
11314 if attrs.on.is_some() {
11315 return Err(syn::Error::new_spanned(
11316 field,
11317 "`on` requires `fk` or `o2o`",
11318 ));
11319 }
11320 Ok(quote!(::core::option::Option::None))
11321 }
11322 }
11323}
11324
11325/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11326/// about kinds without depending on `rustango-core` (which would require a
11327/// proc-macro/normal split it doesn't have today).
11328#[derive(Clone, Copy, PartialEq, Eq)]
11329enum DetectedKind {
11330 I16,
11331 I32,
11332 I64,
11333 F32,
11334 F64,
11335 Bool,
11336 String,
11337 DateTime,
11338 Date,
11339 Time,
11340 Uuid,
11341 Json,
11342 Decimal,
11343 Binary,
11344 /// `Array<String>` → PG `text[]` (#341).
11345 ArrayText,
11346 /// `Array<i32>` → PG `integer[]` (#341).
11347 ArrayInt,
11348 /// `Array<i64>` → PG `bigint[]` (#341).
11349 ArrayBigInt,
11350 /// `Range<i32>` → PG `int4range` (#343).
11351 RangeInt,
11352 /// `Range<i64>` → PG `int8range` (#343).
11353 RangeBigInt,
11354 /// `Range<Decimal>` → PG `numrange` (#343).
11355 RangeNumeric,
11356 /// `Range<NaiveDate>` → PG `daterange` (#343).
11357 RangeDate,
11358 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11359 RangeDateTime,
11360 /// `HStore` → PG `hstore` (#342).
11361 HStore,
11362 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11363 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11364 /// in at the `FieldType` emission site (not carried on this enum).
11365 Vector,
11366 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11367 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11368 /// threaded in at the `FieldType` emission site.
11369 Geometry,
11370}
11371
11372impl DetectedKind {
11373 fn variant_tokens(self) -> TokenStream2 {
11374 let root = rustango_root();
11375 match self {
11376 Self::I16 => quote!(#root::core::FieldType::I16),
11377 Self::I32 => quote!(#root::core::FieldType::I32),
11378 Self::I64 => quote!(#root::core::FieldType::I64),
11379 Self::F32 => quote!(#root::core::FieldType::F32),
11380 Self::F64 => quote!(#root::core::FieldType::F64),
11381 Self::Bool => quote!(#root::core::FieldType::Bool),
11382 Self::String => quote!(#root::core::FieldType::String),
11383 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11384 Self::Date => quote!(#root::core::FieldType::Date),
11385 Self::Time => quote!(#root::core::FieldType::Time),
11386 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11387 Self::Json => quote!(#root::core::FieldType::Json),
11388 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11389 Self::Binary => quote!(#root::core::FieldType::Binary),
11390 Self::ArrayText => {
11391 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11392 }
11393 Self::ArrayInt => {
11394 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11395 }
11396 Self::ArrayBigInt => {
11397 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11398 }
11399 Self::RangeInt => {
11400 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11401 }
11402 Self::RangeBigInt => {
11403 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11404 }
11405 Self::RangeNumeric => {
11406 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11407 }
11408 Self::RangeDate => {
11409 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11410 }
11411 Self::RangeDateTime => {
11412 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11413 }
11414 Self::HStore => quote!(#root::core::FieldType::HStore),
11415 // Dimension comes from the `vector(dims = N)` attribute,
11416 // applied at the emission site; `0` here is just a fallback.
11417 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11418 // SRID comes from the `geometry(srid = N)` attribute, applied
11419 // at the emission site; `0` here is just a fallback.
11420 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11421 }
11422 }
11423
11424 fn is_integer(self) -> bool {
11425 matches!(self, Self::I16 | Self::I32 | Self::I64)
11426 }
11427
11428 /// `(SqlValue::<Variant>, default expr)` for emitting the
11429 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11430 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11431 /// fires only when the parent's `__rustango_pk_value` returns a
11432 /// different variant than expected, which is a compile-time bug —
11433 /// but we still need a value-typed fallback to keep the match
11434 /// total.
11435 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11436 let root = rustango_root();
11437 match self {
11438 Self::I16 => (quote!(I16), quote!(0i16)),
11439 Self::I32 => (quote!(I32), quote!(0i32)),
11440 Self::I64 => (quote!(I64), quote!(0i64)),
11441 Self::F32 => (quote!(F32), quote!(0f32)),
11442 Self::F64 => (quote!(F64), quote!(0f64)),
11443 Self::Bool => (quote!(Bool), quote!(false)),
11444 Self::String => (quote!(String), quote!(::std::string::String::new())),
11445 Self::DateTime => (
11446 quote!(DateTime),
11447 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11448 ),
11449 Self::Date => (
11450 quote!(Date),
11451 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11452 ),
11453 Self::Time => (
11454 quote!(Time),
11455 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11456 ),
11457 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11458 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11459 Self::Decimal => (
11460 quote!(Decimal),
11461 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11462 ),
11463 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11464 // Arrays (#341) can never be a foreign-key primary key, so
11465 // this arm is never reached at runtime — but the match must
11466 // stay total. A bare empty `SqlValue::Array` is the dummy.
11467 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11468 (quote!(Array), quote!(::std::vec::Vec::new()))
11469 }
11470 // Ranges (#343) likewise can't be a FK PK — never reached.
11471 Self::RangeInt
11472 | Self::RangeBigInt
11473 | Self::RangeNumeric
11474 | Self::RangeDate
11475 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11476 // HStore (#342) can't be a FK PK — never reached.
11477 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11478 // Vector (#824) can't be a FK PK — never reached.
11479 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11480 // Geometry (#443) can't be a FK PK — never reached (arm is
11481 // exhaustiveness-only; never interpolated into emitted code).
11482 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11483 }
11484 }
11485}
11486
11487/// Result of walking a field's Rust type. `kind` is the underlying
11488/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11489/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11490/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11491/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11492#[derive(Clone, Copy)]
11493struct DetectedType<'a> {
11494 kind: DetectedKind,
11495 nullable: bool,
11496 auto: bool,
11497 fk_inner: Option<&'a syn::Type>,
11498}
11499
11500/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11501/// non-`Auto` types — the caller should already have routed Auto-only
11502/// codegen through this helper, so a `None` indicates a macro-internal
11503/// invariant break.
11504fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11505 let Type::Path(TypePath { path, qself: None }) = ty else {
11506 return None;
11507 };
11508 let last = path.segments.last()?;
11509 if last.ident != "Auto" {
11510 return None;
11511 }
11512 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11513 return None;
11514 };
11515 args.args.iter().find_map(|a| match a {
11516 syn::GenericArgument::Type(t) => Some(t),
11517 _ => None,
11518 })
11519}
11520
11521fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11522 let Type::Path(TypePath { path, qself: None }) = ty else {
11523 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11524 };
11525 let last = path
11526 .segments
11527 .last()
11528 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11529
11530 if last.ident == "Option" {
11531 let inner = generic_inner(ty, &last.arguments, "Option")?;
11532 let inner_det = detect_type(inner)?;
11533 if inner_det.nullable {
11534 return Err(syn::Error::new_spanned(
11535 ty,
11536 "nested Option is not supported",
11537 ));
11538 }
11539 if inner_det.auto {
11540 return Err(syn::Error::new_spanned(
11541 ty,
11542 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11543 ));
11544 }
11545 return Ok(DetectedType {
11546 nullable: true,
11547 ..inner_det
11548 });
11549 }
11550
11551 if last.ident == "Auto" {
11552 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11553 let inner_det = detect_type(inner)?;
11554 if inner_det.auto {
11555 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11556 }
11557 if inner_det.nullable {
11558 return Err(syn::Error::new_spanned(
11559 ty,
11560 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11561 ));
11562 }
11563 if inner_det.fk_inner.is_some() {
11564 return Err(syn::Error::new_spanned(
11565 ty,
11566 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11567 ));
11568 }
11569 if !matches!(
11570 inner_det.kind,
11571 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11572 ) {
11573 return Err(syn::Error::new_spanned(
11574 ty,
11575 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11576 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11577 (DEFAULT now())",
11578 ));
11579 }
11580 return Ok(DetectedType {
11581 auto: true,
11582 ..inner_det
11583 });
11584 }
11585
11586 if last.ident == "ForeignKey" {
11587 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11588 // Resolve the FK column's underlying SQL type from `K`. When the
11589 // user wrote `ForeignKey<T>` without a key parameter, the type
11590 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11591 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11592 // recurse into K so the column DDL emits the right SQL type
11593 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11594 // emitter knows which `SqlValue` variant to match.
11595 let kind = match key_ty {
11596 Some(k) => detect_type(k)?.kind,
11597 None => DetectedKind::I64,
11598 };
11599 return Ok(DetectedType {
11600 kind,
11601 nullable: false,
11602 auto: false,
11603 fk_inner: Some(inner),
11604 });
11605 }
11606
11607 let kind = match last.ident.to_string().as_str() {
11608 "i16" => DetectedKind::I16,
11609 "i32" => DetectedKind::I32,
11610 "i64" => DetectedKind::I64,
11611 "f32" => DetectedKind::F32,
11612 "f64" => DetectedKind::F64,
11613 "bool" => DetectedKind::Bool,
11614 "String" => DetectedKind::String,
11615 "DateTime" => DetectedKind::DateTime,
11616 "NaiveDate" => DetectedKind::Date,
11617 "NaiveTime" => DetectedKind::Time,
11618 "Uuid" => DetectedKind::Uuid,
11619 "Value" => DetectedKind::Json,
11620 "Decimal" => DetectedKind::Decimal,
11621 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11622 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11623 // — that would emit Binary DDL and decode-fail at runtime.
11624 "Vec" => {
11625 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11626 if let Type::Path(TypePath { path, qself: None }) = inner {
11627 if let Some(seg) = path.segments.last() {
11628 if seg.ident == "u8" && seg.arguments.is_empty() {
11629 return Ok(DetectedType {
11630 kind: DetectedKind::Binary,
11631 nullable: false,
11632 auto: false,
11633 fk_inner: None,
11634 });
11635 }
11636 }
11637 }
11638 return Err(syn::Error::new_spanned(
11639 ty,
11640 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11641 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11642 ));
11643 }
11644 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11645 // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11646 "Array" => {
11647 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11648 let elem = match inner {
11649 Type::Path(TypePath { path, qself: None }) => {
11650 path.segments.last().map(|s| s.ident.to_string())
11651 }
11652 _ => None,
11653 };
11654 let kind = match elem.as_deref() {
11655 Some("String") => DetectedKind::ArrayText,
11656 Some("i32") => DetectedKind::ArrayInt,
11657 Some("i64") => DetectedKind::ArrayBigInt,
11658 _ => {
11659 return Err(syn::Error::new_spanned(
11660 ty,
11661 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11662 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11663 ));
11664 }
11665 };
11666 return Ok(DetectedType {
11667 kind,
11668 nullable: false,
11669 auto: false,
11670 fk_inner: None,
11671 });
11672 }
11673 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11674 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11675 // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11676 // `RangeField` family, #343).
11677 "Range" => {
11678 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11679 let elem = match inner {
11680 Type::Path(TypePath { path, qself: None }) => {
11681 path.segments.last().map(|s| s.ident.to_string())
11682 }
11683 _ => None,
11684 };
11685 let kind = match elem.as_deref() {
11686 Some("i32") => DetectedKind::RangeInt,
11687 Some("i64") => DetectedKind::RangeBigInt,
11688 Some("Decimal") => DetectedKind::RangeNumeric,
11689 Some("NaiveDate") => DetectedKind::RangeDate,
11690 Some("DateTime") => DetectedKind::RangeDateTime,
11691 _ => {
11692 return Err(syn::Error::new_spanned(
11693 ty,
11694 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11695 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11696 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11697 (→ tstzrange) are supported (#343)",
11698 ));
11699 }
11700 };
11701 return Ok(DetectedType {
11702 kind,
11703 nullable: false,
11704 auto: false,
11705 fk_inner: None,
11706 });
11707 }
11708 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11709 // (the `CastValue` impl bridges logical↔stored); the field's own
11710 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11711 // schema just needs `FieldType::String`.
11712 "Cast" => {
11713 return Ok(DetectedType {
11714 kind: DetectedKind::String,
11715 nullable: false,
11716 auto: false,
11717 fk_inner: None,
11718 });
11719 }
11720 // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11721 // parameter — always a string→string map.
11722 "HStore" => {
11723 return Ok(DetectedType {
11724 kind: DetectedKind::HStore,
11725 nullable: false,
11726 auto: false,
11727 fk_inner: None,
11728 });
11729 }
11730 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11731 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11732 "Vector" => {
11733 return Ok(DetectedType {
11734 kind: DetectedKind::Vector,
11735 nullable: false,
11736 auto: false,
11737 fk_inner: None,
11738 });
11739 }
11740 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11741 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11742 "Point" => {
11743 return Ok(DetectedType {
11744 kind: DetectedKind::Geometry,
11745 nullable: false,
11746 auto: false,
11747 fk_inner: None,
11748 });
11749 }
11750 other => {
11751 return Err(syn::Error::new_spanned(
11752 ty,
11753 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)"),
11754 ));
11755 }
11756 };
11757 Ok(DetectedType {
11758 kind,
11759 nullable: false,
11760 auto: false,
11761 fk_inner: None,
11762 })
11763}
11764
11765fn generic_inner<'a>(
11766 ty: &'a Type,
11767 arguments: &'a PathArguments,
11768 wrapper: &str,
11769) -> syn::Result<&'a Type> {
11770 let PathArguments::AngleBracketed(args) = arguments else {
11771 return Err(syn::Error::new_spanned(
11772 ty,
11773 format!("{wrapper} requires a generic argument"),
11774 ));
11775 };
11776 args.args
11777 .iter()
11778 .find_map(|a| match a {
11779 GenericArgument::Type(t) => Some(t),
11780 _ => None,
11781 })
11782 .ok_or_else(|| {
11783 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11784 })
11785}
11786
11787/// Like [`generic_inner`] but pulls *two* type args — the first is
11788/// required, the second is optional. Used by the `ForeignKey<T, K>`
11789/// detection where K defaults to `i64` when omitted.
11790fn generic_pair<'a>(
11791 ty: &'a Type,
11792 arguments: &'a PathArguments,
11793 wrapper: &str,
11794) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11795 let PathArguments::AngleBracketed(args) = arguments else {
11796 return Err(syn::Error::new_spanned(
11797 ty,
11798 format!("{wrapper} requires a generic argument"),
11799 ));
11800 };
11801 let mut types = args.args.iter().filter_map(|a| match a {
11802 GenericArgument::Type(t) => Some(t),
11803 _ => None,
11804 });
11805 let first = types.next().ok_or_else(|| {
11806 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11807 })?;
11808 let second = types.next();
11809 Ok((first, second))
11810}
11811
11812fn to_snake_case(s: &str) -> String {
11813 let mut out = String::with_capacity(s.len() + 4);
11814 for (i, ch) in s.chars().enumerate() {
11815 if ch.is_ascii_uppercase() {
11816 if i > 0 {
11817 out.push('_');
11818 }
11819 out.push(ch.to_ascii_lowercase());
11820 } else {
11821 out.push(ch);
11822 }
11823 }
11824 out
11825}
11826
11827// ============================================================
11828// #[derive(Form)] — slice 8.4B
11829// ============================================================
11830
11831/// Per-field `#[form(...)]` attributes recognised by the derive.
11832#[derive(Default)]
11833struct FormFieldAttrs {
11834 min: Option<i64>,
11835 max: Option<i64>,
11836 min_length: Option<u32>,
11837 max_length: Option<u32>,
11838 /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11839 /// The named static method on the form struct is called after the
11840 /// field's typed parse + length/range checks; it gets the parsed
11841 /// value by reference and returns `Result<<FieldType>, String>`.
11842 /// On Ok, the returned value replaces the parsed one; on Err, the
11843 /// message is attached to the field error list. Issue #372.
11844 clean: Option<syn::Ident>,
11845}
11846
11847/// Container-level `#[form(...)]` attributes. Currently only the
11848/// Django-shape cross-field `validate` hook (issue #373).
11849#[derive(Default)]
11850struct FormContainerAttrs {
11851 /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11852 /// After every per-field parse succeeds, the named method on the
11853 /// form struct is called with `&self` and may return
11854 /// `Result<(), FormErrors>`. Errors merge into the field error
11855 /// list. Issue #373.
11856 validate: Option<syn::Ident>,
11857}
11858
11859/// Detected shape of a form field's Rust type.
11860#[derive(Clone, Copy)]
11861enum FormFieldKind {
11862 String,
11863 I16,
11864 I32,
11865 I64,
11866 F32,
11867 F64,
11868 Bool,
11869}
11870
11871impl FormFieldKind {
11872 fn parse_method(self) -> &'static str {
11873 match self {
11874 Self::I16 => "i16",
11875 Self::I32 => "i32",
11876 Self::I64 => "i64",
11877 Self::F32 => "f32",
11878 Self::F64 => "f64",
11879 // String + Bool don't go through `str::parse`; the codegen
11880 // handles them inline.
11881 Self::String | Self::Bool => "",
11882 }
11883 }
11884}
11885
11886fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11887 let root = rustango_root();
11888 let struct_name = &input.ident;
11889
11890 let Data::Struct(data) = &input.data else {
11891 return Err(syn::Error::new_spanned(
11892 struct_name,
11893 "Form can only be derived on structs",
11894 ));
11895 };
11896 let Fields::Named(named) = &data.fields else {
11897 return Err(syn::Error::new_spanned(
11898 struct_name,
11899 "Form requires a struct with named fields",
11900 ));
11901 };
11902
11903 // #373 — container-level `#[form(validate = "fn")]` hook.
11904 let container = parse_form_container_attrs(input)?;
11905 let post_field_clean: Vec<TokenStream2> = Vec::new();
11906 let _ = post_field_clean;
11907
11908 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
11909 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
11910
11911 for field in &named.named {
11912 let ident = field
11913 .ident
11914 .as_ref()
11915 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11916 let attrs = parse_form_field_attrs(field)?;
11917 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
11918
11919 let name_lit = ident.to_string();
11920 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
11921 // #372 — append the per-field `clean_<field>` call right after
11922 // the parse block when the attribute is set. The clean fn
11923 // takes &T and returns Result<T, String>; on Err we attach
11924 // the message to the field error list without aborting
11925 // (matches Django's "collect all field errors" shape).
11926 let clean_block = if let Some(clean_fn) = &attrs.clean {
11927 quote! {
11928 if __errors.fields().get(#name_lit).is_none() {
11929 match Self::#clean_fn(&#ident) {
11930 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
11931 ::core::result::Result::Err(__msg) => {
11932 __errors.add(#name_lit, __msg);
11933 }
11934 }
11935 }
11936 }
11937 } else {
11938 quote! {}
11939 };
11940 field_blocks.push(quote! {
11941 #parse_block
11942 #clean_block
11943 });
11944 field_idents.push(ident);
11945 }
11946
11947 // #373 — after every per-field parse + clean succeeds, call the
11948 // cross-field validator if declared. Errors merge into the
11949 // outgoing FormErrors via the existing `FormErrors::merge` helper
11950 // (same primitive the DRF serializer cross-field hook uses).
11951 let cross_field_call = if let Some(validate_fn) = &container.validate {
11952 quote! {
11953 if __errors.is_empty() {
11954 let __candidate = Self { #( #field_idents ),* };
11955 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
11956 __errors.merge(__other);
11957 }
11958 if !__errors.is_empty() {
11959 return ::core::result::Result::Err(__errors);
11960 }
11961 return ::core::result::Result::Ok(__candidate);
11962 }
11963 }
11964 } else {
11965 quote! {}
11966 };
11967
11968 Ok(quote! {
11969 impl #root::forms::Form for #struct_name {
11970 fn parse(
11971 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
11972 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
11973 let mut __errors = #root::forms::FormErrors::default();
11974 #( #field_blocks )*
11975 #cross_field_call
11976 if !__errors.is_empty() {
11977 return ::core::result::Result::Err(__errors);
11978 }
11979 ::core::result::Result::Ok(Self {
11980 #( #field_idents ),*
11981 })
11982 }
11983 }
11984 })
11985}
11986
11987fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
11988 let mut out = FormContainerAttrs::default();
11989 for attr in &input.attrs {
11990 if !attr.path().is_ident("form") {
11991 continue;
11992 }
11993 attr.parse_nested_meta(|meta| {
11994 if meta.path.is_ident("validate") {
11995 let s: LitStr = meta.value()?.parse()?;
11996 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
11997 return Ok(());
11998 }
11999 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12000 })?;
12001 }
12002 Ok(out)
12003}
12004
12005fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12006 let mut out = FormFieldAttrs::default();
12007 for attr in &field.attrs {
12008 if !attr.path().is_ident("form") {
12009 continue;
12010 }
12011 attr.parse_nested_meta(|meta| {
12012 if meta.path.is_ident("min") {
12013 let lit: syn::LitInt = meta.value()?.parse()?;
12014 out.min = Some(lit.base10_parse::<i64>()?);
12015 return Ok(());
12016 }
12017 if meta.path.is_ident("max") {
12018 let lit: syn::LitInt = meta.value()?.parse()?;
12019 out.max = Some(lit.base10_parse::<i64>()?);
12020 return Ok(());
12021 }
12022 if meta.path.is_ident("min_length") {
12023 let lit: syn::LitInt = meta.value()?.parse()?;
12024 out.min_length = Some(lit.base10_parse::<u32>()?);
12025 return Ok(());
12026 }
12027 if meta.path.is_ident("max_length") {
12028 let lit: syn::LitInt = meta.value()?.parse()?;
12029 out.max_length = Some(lit.base10_parse::<u32>()?);
12030 return Ok(());
12031 }
12032 if meta.path.is_ident("clean") {
12033 let s: LitStr = meta.value()?.parse()?;
12034 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12035 return Ok(());
12036 }
12037 Err(meta.error(
12038 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12039 ))
12040 })?;
12041 }
12042 Ok(out)
12043}
12044
12045fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12046 let Type::Path(TypePath { path, qself: None }) = ty else {
12047 return Err(syn::Error::new(
12048 span,
12049 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12050 ));
12051 };
12052 let last = path
12053 .segments
12054 .last()
12055 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12056
12057 if last.ident == "Option" {
12058 let inner = generic_inner(ty, &last.arguments, "Option")?;
12059 let (kind, nested) = detect_form_field(inner, span)?;
12060 if nested {
12061 return Err(syn::Error::new(
12062 span,
12063 "nested Option in Form fields is not supported",
12064 ));
12065 }
12066 return Ok((kind, true));
12067 }
12068
12069 let kind = match last.ident.to_string().as_str() {
12070 "String" => FormFieldKind::String,
12071 "i16" => FormFieldKind::I16,
12072 "i32" => FormFieldKind::I32,
12073 "i64" => FormFieldKind::I64,
12074 "f32" => FormFieldKind::F32,
12075 "f64" => FormFieldKind::F64,
12076 "bool" => FormFieldKind::Bool,
12077 other => {
12078 return Err(syn::Error::new(
12079 span,
12080 format!(
12081 "Form field type `{other}` is not supported in v0.8 — use String / \
12082 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12083 ),
12084 ));
12085 }
12086 };
12087 Ok((kind, false))
12088}
12089
12090#[allow(clippy::too_many_lines)]
12091fn render_form_field_parse(
12092 ident: &syn::Ident,
12093 name_lit: &str,
12094 kind: FormFieldKind,
12095 nullable: bool,
12096 attrs: &FormFieldAttrs,
12097) -> TokenStream2 {
12098 // Pull the raw &str from the payload. Uses variable name `data` to
12099 // match the new `Form::parse(data: &HashMap<…>)` signature.
12100 let lookup = quote! {
12101 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12102 };
12103
12104 let parsed_value = match kind {
12105 FormFieldKind::Bool => quote! {
12106 let __v: bool = match __raw {
12107 ::core::option::Option::None => false,
12108 ::core::option::Option::Some(__s) => !matches!(
12109 __s.to_ascii_lowercase().as_str(),
12110 "" | "false" | "0" | "off" | "no"
12111 ),
12112 };
12113 },
12114 FormFieldKind::String => {
12115 if nullable {
12116 quote! {
12117 let __v: ::core::option::Option<::std::string::String> = match __raw {
12118 ::core::option::Option::None => ::core::option::Option::None,
12119 ::core::option::Option::Some(__s) if __s.is_empty() => {
12120 ::core::option::Option::None
12121 }
12122 ::core::option::Option::Some(__s) => {
12123 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12124 }
12125 };
12126 }
12127 } else {
12128 quote! {
12129 let __v: ::std::string::String = match __raw {
12130 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12131 ::core::clone::Clone::clone(__s)
12132 }
12133 _ => {
12134 __errors.add(#name_lit, "This field is required.");
12135 ::std::string::String::new()
12136 }
12137 };
12138 }
12139 }
12140 }
12141 FormFieldKind::I16
12142 | FormFieldKind::I32
12143 | FormFieldKind::I64
12144 | FormFieldKind::F32
12145 | FormFieldKind::F64 => {
12146 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12147 let ty_lit = kind.parse_method();
12148 let default_val = match kind {
12149 FormFieldKind::I16 => quote! { 0i16 },
12150 FormFieldKind::I32 => quote! { 0i32 },
12151 FormFieldKind::I64 => quote! { 0i64 },
12152 FormFieldKind::F32 => quote! { 0f32 },
12153 FormFieldKind::F64 => quote! { 0f64 },
12154 _ => quote! { Default::default() },
12155 };
12156 if nullable {
12157 quote! {
12158 let __v: ::core::option::Option<#parse_ty> = match __raw {
12159 ::core::option::Option::None => ::core::option::Option::None,
12160 ::core::option::Option::Some(__s) if __s.is_empty() => {
12161 ::core::option::Option::None
12162 }
12163 ::core::option::Option::Some(__s) => {
12164 match __s.parse::<#parse_ty>() {
12165 ::core::result::Result::Ok(__n) => {
12166 ::core::option::Option::Some(__n)
12167 }
12168 ::core::result::Result::Err(__e) => {
12169 __errors.add(
12170 #name_lit,
12171 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12172 );
12173 ::core::option::Option::None
12174 }
12175 }
12176 }
12177 };
12178 }
12179 } else {
12180 quote! {
12181 let __v: #parse_ty = match __raw {
12182 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12183 match __s.parse::<#parse_ty>() {
12184 ::core::result::Result::Ok(__n) => __n,
12185 ::core::result::Result::Err(__e) => {
12186 __errors.add(
12187 #name_lit,
12188 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12189 );
12190 #default_val
12191 }
12192 }
12193 }
12194 _ => {
12195 __errors.add(#name_lit, "This field is required.");
12196 #default_val
12197 }
12198 };
12199 }
12200 }
12201 }
12202 };
12203
12204 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12205
12206 quote! {
12207 // `mut` so the per-field `clean` hook (#372) can rewrite the
12208 // parsed value in-place when it returns Ok with a normalized
12209 // form (e.g. trim / lowercase).
12210 let mut #ident = {
12211 #lookup
12212 #parsed_value
12213 #validators
12214 __v
12215 };
12216 }
12217}
12218
12219fn render_form_validators(
12220 name_lit: &str,
12221 kind: FormFieldKind,
12222 nullable: bool,
12223 attrs: &FormFieldAttrs,
12224) -> TokenStream2 {
12225 let mut checks: Vec<TokenStream2> = Vec::new();
12226
12227 let val_ref = if nullable {
12228 quote! { __v.as_ref() }
12229 } else {
12230 quote! { ::core::option::Option::Some(&__v) }
12231 };
12232
12233 let is_string = matches!(kind, FormFieldKind::String);
12234 let is_numeric = matches!(
12235 kind,
12236 FormFieldKind::I16
12237 | FormFieldKind::I32
12238 | FormFieldKind::I64
12239 | FormFieldKind::F32
12240 | FormFieldKind::F64
12241 );
12242
12243 if is_string {
12244 if let Some(min_len) = attrs.min_length {
12245 let min_len_usize = min_len as usize;
12246 checks.push(quote! {
12247 if let ::core::option::Option::Some(__s) = #val_ref {
12248 if __s.len() < #min_len_usize {
12249 __errors.add(
12250 #name_lit,
12251 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12252 );
12253 }
12254 }
12255 });
12256 }
12257 if let Some(max_len) = attrs.max_length {
12258 let max_len_usize = max_len as usize;
12259 checks.push(quote! {
12260 if let ::core::option::Option::Some(__s) = #val_ref {
12261 if __s.len() > #max_len_usize {
12262 __errors.add(
12263 #name_lit,
12264 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12265 );
12266 }
12267 }
12268 });
12269 }
12270 }
12271
12272 if is_numeric {
12273 if let Some(min) = attrs.min {
12274 checks.push(quote! {
12275 if let ::core::option::Option::Some(__n) = #val_ref {
12276 if (*__n as f64) < (#min as f64) {
12277 __errors.add(
12278 #name_lit,
12279 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12280 );
12281 }
12282 }
12283 });
12284 }
12285 if let Some(max) = attrs.max {
12286 checks.push(quote! {
12287 if let ::core::option::Option::Some(__n) = #val_ref {
12288 if (*__n as f64) > (#max as f64) {
12289 __errors.add(
12290 #name_lit,
12291 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12292 );
12293 }
12294 }
12295 });
12296 }
12297 }
12298
12299 quote! { #( #checks )* }
12300}
12301
12302// ============================================================
12303// #[derive(ViewSet)]
12304// ============================================================
12305
12306struct ViewSetAttrs {
12307 model: syn::Path,
12308 fields: Option<Vec<String>>,
12309 filter_fields: Vec<String>,
12310 search_fields: Vec<String>,
12311 /// (field_name, desc)
12312 ordering: Vec<(String, bool)>,
12313 page_size: Option<usize>,
12314 read_only: bool,
12315 perms: ViewSetPermsAttrs,
12316}
12317
12318#[derive(Default)]
12319struct ViewSetPermsAttrs {
12320 list: Vec<String>,
12321 retrieve: Vec<String>,
12322 create: Vec<String>,
12323 update: Vec<String>,
12324 destroy: Vec<String>,
12325}
12326
12327fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12328 let root = rustango_root();
12329 let struct_name = &input.ident;
12330
12331 // Must be a unit struct or an empty named struct.
12332 match &input.data {
12333 Data::Struct(s) => match &s.fields {
12334 Fields::Unit | Fields::Named(_) => {}
12335 Fields::Unnamed(_) => {
12336 return Err(syn::Error::new_spanned(
12337 struct_name,
12338 "ViewSet can only be derived on a unit struct or an empty named struct",
12339 ));
12340 }
12341 },
12342 _ => {
12343 return Err(syn::Error::new_spanned(
12344 struct_name,
12345 "ViewSet can only be derived on a struct",
12346 ));
12347 }
12348 }
12349
12350 let attrs = parse_viewset_attrs(input)?;
12351 let model_path = &attrs.model;
12352
12353 // `.fields(&[...])` call — None means skip (use all scalar fields).
12354 let fields_call = if let Some(ref fields) = attrs.fields {
12355 let lits = fields.iter().map(|f| f.as_str());
12356 quote!(.fields(&[ #(#lits),* ]))
12357 } else {
12358 quote!()
12359 };
12360
12361 let filter_fields_call = if attrs.filter_fields.is_empty() {
12362 quote!()
12363 } else {
12364 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12365 quote!(.filter_fields(&[ #(#lits),* ]))
12366 };
12367
12368 let search_fields_call = if attrs.search_fields.is_empty() {
12369 quote!()
12370 } else {
12371 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12372 quote!(.search_fields(&[ #(#lits),* ]))
12373 };
12374
12375 let ordering_call = if attrs.ordering.is_empty() {
12376 quote!()
12377 } else {
12378 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12379 let f = f.as_str();
12380 quote!((#f, #desc))
12381 });
12382 quote!(.ordering(&[ #(#pairs),* ]))
12383 };
12384
12385 let page_size_call = if let Some(n) = attrs.page_size {
12386 quote!(.page_size(#n))
12387 } else {
12388 quote!()
12389 };
12390
12391 let read_only_call = if attrs.read_only {
12392 quote!(.read_only())
12393 } else {
12394 quote!()
12395 };
12396
12397 let perms = &attrs.perms;
12398 let perms_call = if perms.list.is_empty()
12399 && perms.retrieve.is_empty()
12400 && perms.create.is_empty()
12401 && perms.update.is_empty()
12402 && perms.destroy.is_empty()
12403 {
12404 quote!()
12405 } else {
12406 let list_lits = perms.list.iter().map(|s| s.as_str());
12407 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12408 let create_lits = perms.create.iter().map(|s| s.as_str());
12409 let update_lits = perms.update.iter().map(|s| s.as_str());
12410 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12411 quote! {
12412 .permissions(#root::viewset::ViewSetPerms {
12413 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12414 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12415 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12416 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12417 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12418 })
12419 }
12420 };
12421
12422 Ok(quote! {
12423 impl #struct_name {
12424 /// Build an `axum::Router` with the six standard REST endpoints
12425 /// for this ViewSet, mounted at `prefix`.
12426 pub fn router(prefix: &str, pool: #root::sql::sqlx::PgPool) -> #root::__axum::Router {
12427 #root::viewset::ViewSet::for_model(
12428 <#model_path as #root::core::Model>::SCHEMA
12429 )
12430 #fields_call
12431 #filter_fields_call
12432 #search_fields_call
12433 #ordering_call
12434 #page_size_call
12435 #perms_call
12436 #read_only_call
12437 .router(prefix, pool)
12438 }
12439 }
12440 })
12441}
12442
12443fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12444 let mut model: Option<syn::Path> = None;
12445 let mut fields: Option<Vec<String>> = None;
12446 let mut filter_fields: Vec<String> = Vec::new();
12447 let mut search_fields: Vec<String> = Vec::new();
12448 let mut ordering: Vec<(String, bool)> = Vec::new();
12449 let mut page_size: Option<usize> = None;
12450 let mut read_only = false;
12451 let mut perms = ViewSetPermsAttrs::default();
12452
12453 for attr in &input.attrs {
12454 if !attr.path().is_ident("viewset") {
12455 continue;
12456 }
12457 attr.parse_nested_meta(|meta| {
12458 if meta.path.is_ident("model") {
12459 let path: syn::Path = meta.value()?.parse()?;
12460 model = Some(path);
12461 return Ok(());
12462 }
12463 if meta.path.is_ident("fields") {
12464 let s: LitStr = meta.value()?.parse()?;
12465 fields = Some(split_field_list(&s.value()));
12466 return Ok(());
12467 }
12468 if meta.path.is_ident("filter_fields") {
12469 let s: LitStr = meta.value()?.parse()?;
12470 filter_fields = split_field_list(&s.value());
12471 return Ok(());
12472 }
12473 if meta.path.is_ident("search_fields") {
12474 let s: LitStr = meta.value()?.parse()?;
12475 search_fields = split_field_list(&s.value());
12476 return Ok(());
12477 }
12478 if meta.path.is_ident("ordering") {
12479 let s: LitStr = meta.value()?.parse()?;
12480 ordering = parse_ordering_list(&s.value());
12481 return Ok(());
12482 }
12483 if meta.path.is_ident("page_size") {
12484 let lit: syn::LitInt = meta.value()?.parse()?;
12485 page_size = Some(lit.base10_parse::<usize>()?);
12486 return Ok(());
12487 }
12488 if meta.path.is_ident("read_only") {
12489 read_only = true;
12490 return Ok(());
12491 }
12492 if meta.path.is_ident("permissions") {
12493 meta.parse_nested_meta(|inner| {
12494 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12495 let s: LitStr = inner.value()?.parse()?;
12496 Ok(split_field_list(&s.value()))
12497 };
12498 if inner.path.is_ident("list") {
12499 perms.list = parse_codenames(&inner)?;
12500 } else if inner.path.is_ident("retrieve") {
12501 perms.retrieve = parse_codenames(&inner)?;
12502 } else if inner.path.is_ident("create") {
12503 perms.create = parse_codenames(&inner)?;
12504 } else if inner.path.is_ident("update") {
12505 perms.update = parse_codenames(&inner)?;
12506 } else if inner.path.is_ident("destroy") {
12507 perms.destroy = parse_codenames(&inner)?;
12508 } else {
12509 return Err(inner.error(
12510 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12511 ));
12512 }
12513 Ok(())
12514 })?;
12515 return Ok(());
12516 }
12517 Err(meta.error(
12518 "unknown viewset attribute (supported: model, fields, filter_fields, \
12519 search_fields, ordering, page_size, read_only, permissions(...))",
12520 ))
12521 })?;
12522 }
12523
12524 let model = model.ok_or_else(|| {
12525 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12526 })?;
12527
12528 Ok(ViewSetAttrs {
12529 model,
12530 fields,
12531 filter_fields,
12532 search_fields,
12533 ordering,
12534 page_size,
12535 read_only,
12536 perms,
12537 })
12538}
12539
12540// ============================================================ #[derive(Serializer)]
12541
12542struct SerializerContainerAttrs {
12543 model: syn::Path,
12544 /// `#[serializer(validate = "fn_name")]` on the struct — DRF-shape
12545 /// cross-field validation hook (#436). The named inherent method
12546 /// must take `&self` and return
12547 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12548 /// `validate()` runs every per-field validator first; then calls
12549 /// the cross-field method if declared; aggregates all errors into
12550 /// one `FormErrors`.
12551 cross_validate: Option<syn::Ident>,
12552}
12553
12554#[derive(Default)]
12555struct SerializerFieldAttrs {
12556 read_only: bool,
12557 write_only: bool,
12558 source: Option<String>,
12559 skip: bool,
12560 /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
12561 /// analog. The macro emits `from_model` initializer that calls
12562 /// `Self::fn_name(&model)` and stores the return value.
12563 method: Option<String>,
12564 /// `#[serializer(validate = "fn_name")]` — per-field validator
12565 /// callable run by `Self::validate(&self)`. Must return
12566 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12567 /// the field name.
12568 validate: Option<String>,
12569 /// `#[serializer(nested)]` on a field whose type is another
12570 /// `Serializer` — the macro emits `from_model` initializer that
12571 /// reads the parent via `model.<source>.value()` then calls the
12572 /// child serializer's `from_model(parent)`. When the FK is
12573 /// unloaded the field falls back to `Default::default()` (does
12574 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12575 /// Source field on the model defaults to the field name; override
12576 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12577 /// panic-on-unloaded behavior for tests.
12578 nested: bool,
12579 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12580 /// strict behavior: panic when the FK isn't loaded. Useful in
12581 /// test code where forgetting select_related must trip a hard
12582 /// failure rather than render a blank nested object.
12583 nested_strict: bool,
12584 /// `#[serializer(many = TagSerializer)]` — declare the field as
12585 /// a list of nested serializers. Field type must be `Vec<S>`
12586 /// where `S` is the inner serializer. The macro initializes the
12587 /// field to `Vec::new()` in `from_model` and emits a typed
12588 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12589 /// maps each model row through `S::from_model`. Auto-load isn't
12590 /// possible (the M2M / one-to-many accessor is async); callers
12591 /// fetch the children + call the setter post-from_model.
12592 many: Option<syn::Type>,
12593 /// `#[serializer(slug = "name")]` — DRF `SlugRelatedField` analog.
12594 /// Source field on the model must be a `ForeignKey<T>`; the
12595 /// macro emits `from_model` glue that walks
12596 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12597 /// the serializer is typically `String` (whatever type the slug
12598 /// column has). When the FK is unloaded the field falls back to
12599 /// `Default::default()`, same graceful-degrade contract as
12600 /// `nested`. Source defaults to the field name; override with
12601 /// `source = "..."`. v0.44.
12602 slug: Option<String>,
12603}
12604
12605fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12606 let mut model: Option<syn::Path> = None;
12607 let mut cross_validate: Option<syn::Ident> = None;
12608 for attr in &input.attrs {
12609 if !attr.path().is_ident("serializer") {
12610 continue;
12611 }
12612 attr.parse_nested_meta(|meta| {
12613 if meta.path.is_ident("model") {
12614 let _eq: syn::Token![=] = meta.input.parse()?;
12615 model = Some(meta.input.parse()?);
12616 return Ok(());
12617 }
12618 if meta.path.is_ident("validate") {
12619 // #436 — container-level `validate = "fn_name"` for the
12620 // DRF cross-field-validation shape. Field-level
12621 // `#[serializer(validate = "...")]` on a field is
12622 // parsed separately in `parse_serializer_field_attrs`.
12623 let s: LitStr = meta.value()?.parse()?;
12624 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12625 return Ok(());
12626 }
12627 Err(meta.error(
12628 "unknown serializer container attribute \
12629 (supported: `model`, `validate`)",
12630 ))
12631 })?;
12632 }
12633 let model = model.ok_or_else(|| {
12634 syn::Error::new_spanned(
12635 &input.ident,
12636 "`#[serializer(model = SomeModel)]` is required",
12637 )
12638 })?;
12639 Ok(SerializerContainerAttrs {
12640 model,
12641 cross_validate,
12642 })
12643}
12644
12645fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12646 let mut out = SerializerFieldAttrs::default();
12647 for attr in &field.attrs {
12648 if !attr.path().is_ident("serializer") {
12649 continue;
12650 }
12651 attr.parse_nested_meta(|meta| {
12652 if meta.path.is_ident("read_only") {
12653 out.read_only = true;
12654 return Ok(());
12655 }
12656 if meta.path.is_ident("write_only") {
12657 out.write_only = true;
12658 return Ok(());
12659 }
12660 if meta.path.is_ident("skip") {
12661 out.skip = true;
12662 return Ok(());
12663 }
12664 if meta.path.is_ident("source") {
12665 let s: LitStr = meta.value()?.parse()?;
12666 out.source = Some(s.value());
12667 return Ok(());
12668 }
12669 if meta.path.is_ident("method") {
12670 let s: LitStr = meta.value()?.parse()?;
12671 out.method = Some(s.value());
12672 return Ok(());
12673 }
12674 if meta.path.is_ident("validate") {
12675 let s: LitStr = meta.value()?.parse()?;
12676 out.validate = Some(s.value());
12677 return Ok(());
12678 }
12679 if meta.path.is_ident("many") {
12680 let _eq: syn::Token![=] = meta.input.parse()?;
12681 out.many = Some(meta.input.parse()?);
12682 return Ok(());
12683 }
12684 if meta.path.is_ident("nested") {
12685 out.nested = true;
12686 // Optional strict flag inside parentheses:
12687 // #[serializer(nested(strict))]
12688 if meta.input.peek(syn::token::Paren) {
12689 meta.parse_nested_meta(|inner| {
12690 if inner.path.is_ident("strict") {
12691 out.nested_strict = true;
12692 return Ok(());
12693 }
12694 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12695 })?;
12696 }
12697 return Ok(());
12698 }
12699 if meta.path.is_ident("slug") {
12700 let s: LitStr = meta.value()?.parse()?;
12701 out.slug = Some(s.value());
12702 return Ok(());
12703 }
12704 Err(meta.error(
12705 "unknown serializer field attribute (supported: \
12706 `read_only`, `write_only`, `source`, `skip`, `method`, \
12707 `validate`, `nested`, `many`, `slug`)",
12708 ))
12709 })?;
12710 }
12711 // Validate: read_only + write_only is nonsensical
12712 if out.read_only && out.write_only {
12713 return Err(syn::Error::new_spanned(
12714 field,
12715 "a field cannot be both `read_only` and `write_only`",
12716 ));
12717 }
12718 if out.method.is_some() && out.source.is_some() {
12719 return Err(syn::Error::new_spanned(
12720 field,
12721 "`method` and `source` are mutually exclusive — `method` computes \
12722 the value from a method, `source` reads it from a different model field",
12723 ));
12724 }
12725 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12726 return Err(syn::Error::new_spanned(
12727 field,
12728 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12729 — pick one strategy for populating the field",
12730 ));
12731 }
12732 Ok(out)
12733}
12734
12735fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12736 let root = rustango_root();
12737 let struct_name = &input.ident;
12738 let struct_name_lit = struct_name.to_string();
12739
12740 let Data::Struct(data) = &input.data else {
12741 return Err(syn::Error::new_spanned(
12742 struct_name,
12743 "Serializer can only be derived on structs",
12744 ));
12745 };
12746 let Fields::Named(named) = &data.fields else {
12747 return Err(syn::Error::new_spanned(
12748 struct_name,
12749 "Serializer requires a struct with named fields",
12750 ));
12751 };
12752
12753 let container = parse_serializer_container_attrs(input)?;
12754 let model_path = &container.model;
12755
12756 // Classify each field. `ty` is only consumed by the
12757 // `#[cfg(feature = "openapi")]` block below, but we always
12758 // capture it to keep the field-info build a single pass.
12759 #[allow(dead_code)]
12760 struct FieldInfo {
12761 ident: syn::Ident,
12762 ty: syn::Type,
12763 attrs: SerializerFieldAttrs,
12764 }
12765 let mut fields_info: Vec<FieldInfo> = Vec::new();
12766 for field in &named.named {
12767 let ident = field.ident.clone().expect("named field has ident");
12768 let attrs = parse_serializer_field_attrs(field)?;
12769 fields_info.push(FieldInfo {
12770 ident,
12771 ty: field.ty.clone(),
12772 attrs,
12773 });
12774 }
12775
12776 // Generate from_model body: struct literal with each field assigned.
12777 let from_model_fields = fields_info.iter().map(|fi| {
12778 let ident = &fi.ident;
12779 let ty = &fi.ty;
12780 if let Some(_inner) = &fi.attrs.many {
12781 // Many — collection field. Initialize empty; caller
12782 // populates via the macro-emitted set_<field> helper
12783 // after fetching the M2M children.
12784 quote! { #ident: ::std::vec::Vec::new() }
12785 } else if let Some(method) = &fi.attrs.method {
12786 // SerializerMethodField: call Self::<method>(&model) to
12787 // compute the value. Method signature must be
12788 // `fn <method>(model: &T) -> <field type>`.
12789 let method_ident = syn::Ident::new(method, ident.span());
12790 quote! { #ident: Self::#method_ident(model) }
12791 } else if let Some(slug_field) = &fi.attrs.slug {
12792 // v0.44 — SlugRelatedField. Source defaults to the field
12793 // name on this struct; override via `source = "..."`. The
12794 // source field on the model is expected to be a
12795 // `ForeignKey<T>`; the slug field on the parent is named
12796 // by the attribute value. When the FK is unloaded the
12797 // field falls back to `Default::default()` — same
12798 // graceful-degrade contract as `nested`.
12799 let src_name = fi
12800 .attrs
12801 .source
12802 .as_deref()
12803 .unwrap_or(&fi.ident.to_string())
12804 .to_owned();
12805 let src_ident = syn::Ident::new(&src_name, ident.span());
12806 let slug_ident = syn::Ident::new(slug_field, ident.span());
12807 quote! {
12808 #ident: match model.#src_ident.value() {
12809 ::core::option::Option::Some(__loaded) =>
12810 ::core::clone::Clone::clone(&__loaded.#slug_ident),
12811 ::core::option::Option::None =>
12812 ::core::default::Default::default(),
12813 }
12814 }
12815 } else if fi.attrs.nested {
12816 // Nested serializer. Source defaults to the field name on
12817 // this struct; override via `source = "..."`. The source
12818 // field on the model is expected to be a `ForeignKey<T>`
12819 // whose `.value()` returns `Option<&T>` after lazy-load.
12820 //
12821 // Behavior matrix (tweakable per-field):
12822 // * FK loaded → nested object materializes via
12823 // ChildSerializer::from_model(parent).
12824 // * FK unloaded → fall back to ChildSerializer::default()
12825 // (so prod doesn't crash on a missing
12826 // prefetch — just renders a blank nested
12827 // object). Add `#[serializer(nested,
12828 // strict)]` to keep the v0.18.1
12829 // panic-on-unloaded behavior for tests
12830 // that want hard guardrails.
12831 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
12832 let src_ident = syn::Ident::new(&src_name, ident.span());
12833 if fi.attrs.nested_strict {
12834 let panic_msg = format!(
12835 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
12836 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
12837 );
12838 quote! {
12839 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
12840 model.#src_ident.value().expect(#panic_msg),
12841 )
12842 }
12843 } else {
12844 quote! {
12845 #ident: match model.#src_ident.value() {
12846 ::core::option::Option::Some(__loaded) =>
12847 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
12848 ::core::option::Option::None =>
12849 ::core::default::Default::default(),
12850 }
12851 }
12852 }
12853 } else if fi.attrs.write_only || fi.attrs.skip {
12854 // Not read from model — use default
12855 quote! { #ident: ::core::default::Default::default() }
12856 } else if let Some(src) = &fi.attrs.source {
12857 let src_ident = syn::Ident::new(src, ident.span());
12858 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
12859 } else {
12860 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
12861 }
12862 });
12863
12864 // Per-field validators (DRF-shape `validators=[...]`). Emit a
12865 // `validate(&self)` method that runs each user-defined validator
12866 // and aggregates errors into `FormErrors`.
12867 let validator_calls: Vec<_> = fields_info
12868 .iter()
12869 .filter_map(|fi| {
12870 let ident = &fi.ident;
12871 let name_lit = ident.to_string();
12872 let method = fi.attrs.validate.as_ref()?;
12873 let method_ident = syn::Ident::new(method, ident.span());
12874 Some(quote! {
12875 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
12876 __errors.add(#name_lit.to_owned(), __e);
12877 }
12878 })
12879 })
12880 .collect();
12881 // #436 — DRF cross-field `validate(self)` shape. If the
12882 // container declared `#[serializer(validate = "fn_name")]`,
12883 // the macro-generated `validate(&self)` runs every per-field
12884 // validator first, then calls the user's cross-field method,
12885 // merging its `FormErrors` into the per-field errors. Either
12886 // alone is enough to emit the wrapper.
12887 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
12888 quote! {
12889 // Merge cross-field errors into the per-field bucket so
12890 // a single .validate() call surfaces both layers.
12891 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
12892 __errors.merge(__cross);
12893 }
12894 }
12895 });
12896 let validate_method = if validator_calls.is_empty() && container.cross_validate.is_none() {
12897 quote! {}
12898 } else {
12899 quote! {
12900 impl #struct_name {
12901 /// Run every `#[serializer(validate = "...")]` per-field
12902 /// validator and, when declared, the container-level
12903 /// cross-field validator. Aggregates errors into
12904 /// `FormErrors` keyed by the field name (plus any
12905 /// non-field keys the cross-field method adds).
12906 /// Returns `Ok(())` when all pass.
12907 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
12908 let mut __errors = #root::forms::FormErrors::default();
12909 #( #validator_calls )*
12910 #cross_validate_call
12911 if __errors.is_empty() {
12912 ::core::result::Result::Ok(())
12913 } else {
12914 ::core::result::Result::Err(__errors)
12915 }
12916 }
12917 }
12918 }
12919 };
12920
12921 // For every `#[serializer(many = S)]` field, emit a
12922 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
12923 // helper that maps the parents through `S::from_model`.
12924 let many_setters: Vec<_> = fields_info
12925 .iter()
12926 .filter_map(|fi| {
12927 let many_ty = fi.attrs.many.as_ref()?;
12928 let ident = &fi.ident;
12929 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
12930 Some(quote! {
12931 /// Populate this `many` field by mapping each parent model
12932 /// through the inner serializer's `from_model`. Call after
12933 /// fetching the M2M / one-to-many children since
12934 /// `from_model` itself can't await an SQL query.
12935 pub fn #setter(
12936 &mut self,
12937 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
12938 ) -> &mut Self {
12939 self.#ident = models.iter()
12940 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
12941 .collect();
12942 self
12943 }
12944 })
12945 })
12946 .collect();
12947 let many_setters_impl = if many_setters.is_empty() {
12948 quote! {}
12949 } else {
12950 quote! {
12951 impl #struct_name {
12952 #( #many_setters )*
12953 }
12954 }
12955 };
12956
12957 // Generate custom Serialize: skip write_only fields
12958 let output_fields: Vec<_> = fields_info
12959 .iter()
12960 .filter(|fi| !fi.attrs.write_only)
12961 .collect();
12962 let output_field_count = output_fields.len();
12963 let serialize_fields = output_fields.iter().map(|fi| {
12964 let ident = &fi.ident;
12965 let name_lit = ident.to_string();
12966 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
12967 });
12968
12969 // writable_fields: normal + write_only.
12970 // Exclude:
12971 // - `read_only` — server-computed.
12972 // - `skip` — caller sets manually post-from_model.
12973 // - `method` — computed from a Self::fn(&model) call; accepting
12974 // it on write is meaningless.
12975 // - `nested` / `many` — populated from related-model data, not
12976 // from a field on the wire body.
12977 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
12978 // `many` in `writable_fields()`, which made the ViewSet write
12979 // path accept those fields from the JSON body and try to bind
12980 // them to the SQL UPDATE — a silent no-op at best, a type
12981 // mismatch at worst.
12982 let writable_lits: Vec<_> = fields_info
12983 .iter()
12984 .filter(|fi| {
12985 !fi.attrs.read_only
12986 && !fi.attrs.skip
12987 && fi.attrs.method.is_none()
12988 && !fi.attrs.nested
12989 && fi.attrs.many.is_none()
12990 && fi.attrs.slug.is_none()
12991 })
12992 .map(|fi| fi.ident.to_string())
12993 .collect();
12994
12995 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
12996 // Only includes fields shown in JSON output (skips write_only). For each
12997 // `Option<T>` field, omit from `required` and add `.nullable()`.
12998 let openapi_impl = {
12999 #[cfg(feature = "openapi")]
13000 {
13001 let property_calls = output_fields.iter().map(|fi| {
13002 let ident = &fi.ident;
13003 let name_lit = ident.to_string();
13004 let ty = &fi.ty;
13005 let nullable_call = if is_option(ty) {
13006 quote! { .nullable() }
13007 } else {
13008 quote! {}
13009 };
13010 quote! {
13011 .property(
13012 #name_lit,
13013 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13014 #nullable_call,
13015 )
13016 }
13017 });
13018 let required_lits: Vec<_> = output_fields
13019 .iter()
13020 .filter(|fi| !is_option(&fi.ty))
13021 .map(|fi| fi.ident.to_string())
13022 .collect();
13023 quote! {
13024 impl #root::openapi::OpenApiSchema for #struct_name {
13025 fn openapi_schema() -> #root::openapi::Schema {
13026 #root::openapi::Schema::object()
13027 #( #property_calls )*
13028 .required([ #( #required_lits ),* ])
13029 }
13030 }
13031 }
13032 }
13033 #[cfg(not(feature = "openapi"))]
13034 {
13035 quote! {}
13036 }
13037 };
13038
13039 Ok(quote! {
13040 impl #root::serializer::ModelSerializer for #struct_name {
13041 type Model = #model_path;
13042
13043 fn from_model(model: &Self::Model) -> Self {
13044 Self {
13045 #( #from_model_fields ),*
13046 }
13047 }
13048
13049 fn writable_fields() -> &'static [&'static str] {
13050 &[ #( #writable_lits ),* ]
13051 }
13052 }
13053
13054 impl #root::__serde::Serialize for #struct_name {
13055 fn serialize<S>(&self, serializer: S)
13056 -> ::core::result::Result<S::Ok, S::Error>
13057 where
13058 S: #root::__serde::Serializer,
13059 {
13060 use #root::__serde::ser::SerializeStruct;
13061 let mut __state = serializer.serialize_struct(
13062 #struct_name_lit,
13063 #output_field_count,
13064 )?;
13065 #( #serialize_fields )*
13066 __state.end()
13067 }
13068 }
13069
13070 #openapi_impl
13071
13072 #validate_method
13073
13074 #many_setters_impl
13075 })
13076}
13077
13078/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13079/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13080/// when the feature is off.
13081#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13082fn is_option(ty: &syn::Type) -> bool {
13083 if let syn::Type::Path(p) = ty {
13084 if let Some(last) = p.path.segments.last() {
13085 return last.ident == "Option";
13086 }
13087 }
13088 false
13089}