Skip to main content

pgorm_derive/
lib.rs

1//! Derive macros for pgorm
2//!
3//! Provides `#[derive(FromRow)]` and `#[derive(Model)]` macros.
4
5use proc_macro::TokenStream;
6use syn::{DeriveInput, parse_macro_input};
7
8mod common;
9mod from_row;
10mod insert_model;
11mod model;
12mod query_params;
13mod sql_ident;
14mod update_model;
15
16/// Derive `FromRow` trait for a struct.
17///
18/// # Example
19///
20/// ```ignore
21/// use pgorm::FromRow;
22///
23/// #[derive(FromRow)]
24/// struct User {
25///     id: i64,
26///     username: String,
27///     #[orm(column = "email_address")]
28///     email: Option<String>,
29/// }
30/// ```
31///
32/// # Attributes
33///
34/// - `#[orm(column = "name")]` - Map field to a different column name
35#[proc_macro_derive(FromRow, attributes(orm))]
36pub fn derive_from_row(input: TokenStream) -> TokenStream {
37    let input = parse_macro_input!(input as DeriveInput);
38    from_row::expand(input)
39        .unwrap_or_else(|e| e.to_compile_error())
40        .into()
41}
42
43/// Derive `Model` metadata for a struct.
44///
45/// # Example
46///
47/// ```ignore
48/// use pgorm::Model;
49///
50/// #[derive(Model)]
51/// #[orm(table = "users")]
52/// struct User {
53///     #[orm(id)]
54///     user_id: i64,
55///     username: String,
56///     email: Option<String>,
57/// }
58/// ```
59///
60/// # Generated
61///
62/// - `TABLE: &'static str` - Table name
63/// - `COL_*: &'static str` - Column name constants
64/// - `SELECT_LIST: &'static str` - Comma-separated column list
65/// - `fn select_list_as(alias: &str) -> String` - Aliased column list for JOINs
66///
67/// # Attributes
68///
69/// Struct-level:
70///
71/// - `#[orm(table = "name")]` - Specify table name (required)
72/// - `#[orm(join(table = "...", on = "...", type = "inner|left|right|full|cross"))]` - Add JOINs (optional, repeatable)
73/// - `#[orm(has_many(ChildType, foreign_key = "...", as = "..."))]` - Generate select_has_many helpers (optional, repeatable)
74/// - `#[orm(belongs_to(ParentType, foreign_key = "...", as = "..."))]` - Generate select_belongs_to helpers (optional, repeatable)
75///
76/// Field-level:
77///
78/// - `#[orm(id)]` - Mark field as primary key
79/// - `#[orm(column = "name")]` - Map field to a different column name
80/// - `#[orm(table = "name")]` - Mark field as coming from a joined table (for view/join models)
81#[proc_macro_derive(Model, attributes(orm))]
82pub fn derive_model(input: TokenStream) -> TokenStream {
83    let input = parse_macro_input!(input as DeriveInput);
84    model::expand(input)
85        .unwrap_or_else(|e| e.to_compile_error())
86        .into()
87}
88
89/// Derive `ViewModel` metadata for a struct.
90///
91/// This is an alias of `Model` intended to express that the type is a read/view model
92/// (optionally including JOINs), while write models are derived separately.
93#[proc_macro_derive(ViewModel, attributes(orm))]
94pub fn derive_view_model(input: TokenStream) -> TokenStream {
95    let input = parse_macro_input!(input as DeriveInput);
96    model::expand(input)
97        .unwrap_or_else(|e| e.to_compile_error())
98        .into()
99}
100
101/// Derive `InsertModel` helpers for inserting into a table.
102///
103/// # Attributes
104///
105/// Struct-level:
106///
107/// - `#[orm(table = "name")]` - Specify table name (required)
108/// - `#[orm(returning = "TypePath")]` - Enable `insert_returning` helpers (optional)
109/// - Conflict handling (Postgres `ON CONFLICT`):
110///   - `#[orm(conflict_target = "col1,col2")]` - conflict target columns (optional)
111///   - `#[orm(conflict_constraint = "constraint_name")]` - conflict constraint (optional)
112///   - `#[orm(conflict_update = "col1,col2")]` - columns to update on conflict (optional)
113/// - Multi-table write graphs (advanced): function-style attrs like `#[orm(has_many(...))]`,
114///   `#[orm(belongs_to(...))]`, `#[orm(before_insert(...))]`. See `docs/design/multi-table-writes-final.md`.
115///
116/// Field-level:
117///
118/// - `#[orm(id)]` - Mark field as primary key (optional)
119/// - `#[orm(skip_insert)]` - Never include this field in INSERT
120/// - `#[orm(default)]` - Use SQL `DEFAULT` for this field
121/// - `#[orm(auto_now_add)]` - Use `NOW()` for this field on insert
122/// - `#[orm(column = "name")]` / `#[orm(table = "name")]` - Override column/table mapping (optional)
123#[proc_macro_derive(InsertModel, attributes(orm))]
124pub fn derive_insert_model(input: TokenStream) -> TokenStream {
125    let input = parse_macro_input!(input as DeriveInput);
126    insert_model::expand(input)
127        .unwrap_or_else(|e| e.to_compile_error())
128        .into()
129}
130
131/// Derive `UpdateModel` helpers for updating a table (patch-style).
132///
133/// # Attributes
134///
135/// Struct-level:
136///
137/// - `#[orm(table = "name")]` - Specify table name (required)
138/// - One of:
139///   - `#[orm(id_column = "id")]` - Explicit primary key column
140///   - `#[orm(model = "TypePath")]` - Derive primary key column from a `Model`
141///   - `#[orm(returning = "TypePath")]` where `TypePath::ID` exists
142/// - `#[orm(returning = "TypePath")]` - Enable `update_by_id_returning` helpers (optional)
143/// - Multi-table write graphs (advanced): see `docs/design/multi-table-writes-final.md`.
144///
145/// Field-level:
146///
147/// - `#[orm(skip_update)]` - Never include this field in UPDATE
148/// - `#[orm(default)]` - Use SQL `DEFAULT` for this field
149/// - `#[orm(auto_now)]` - Use `NOW()` for this field on update
150/// - `#[orm(column = "name")]` / `#[orm(table = "name")]` - Override column/table mapping (optional)
151#[proc_macro_derive(UpdateModel, attributes(orm))]
152pub fn derive_update_model(input: TokenStream) -> TokenStream {
153    let input = parse_macro_input!(input as DeriveInput);
154    update_model::expand(input)
155        .unwrap_or_else(|e| e.to_compile_error())
156        .into()
157}
158
159/// Derive `QueryParams` helpers for building dynamic queries from a params struct.
160///
161/// # Example
162///
163/// ```ignore
164/// use pgorm::QueryParams;
165///
166/// #[derive(QueryParams)]
167/// #[orm(model = "User")]
168/// struct UserSearchParams<'a> {
169///     #[orm(eq(UserQuery::COL_ID))]
170///     id: Option<i64>,
171///     #[orm(eq(UserQuery::COL_EMAIL))]
172///     email: Option<&'a str>,
173/// }
174///
175/// let q = UserSearchParams { id, email }.into_query()?;
176/// ```
177///
178/// # Attributes
179///
180/// Struct-level:
181/// - `#[orm(model = "TypePath")]` - The model type that provides `Model::query()`
182///
183/// Field-level:
184/// - `#[orm(eq(COL))]` - Equality filter (auto uses `eq_opt_str` for `&str`/`String`)
185/// - `#[orm(eq_str(COL))]` - Equality filter, forcing string conversion
186/// - `#[orm(eq_map(COL, map_fn))]` - Equality filter after mapping (e.g. parse)
187/// - `#[orm(map(map_fn))]` - Optional mapper (returns `Option<T>`; `None` means "skip filter")
188/// - `#[orm(ne(COL))]` / `#[orm(gt(COL))]` / `#[orm(gte(COL))]` / `#[orm(lt(COL))]` / `#[orm(lte(COL))]`
189/// - `#[orm(like(COL))]` / `#[orm(ilike(COL))]` / `#[orm(not_like(COL))]` / `#[orm(not_ilike(COL))]`
190/// - `#[orm(in_list(COL))]` / `#[orm(not_in(COL))]`
191/// - `#[orm(between(COL))]` / `#[orm(not_between(COL))]` (expects `(T, T)` or `Option<(T, T)>`)
192/// - `#[orm(is_null(COL))]` / `#[orm(is_not_null(COL))]` (expects `bool` or `Option<bool>`)
193/// - `#[orm(order_by)]` - Replace the `OrderBy` builder (expects `OrderBy` or `Option<OrderBy>`)
194/// - `#[orm(order_by_asc)]` / `#[orm(order_by_desc)]` - Add an ORDER BY column (expects a column ident or `Option<...>`)
195/// - `#[orm(order_by_raw)]` - Add a raw ORDER BY item (escape hatch)
196/// - `#[orm(paginate)]` - Replace the `Pagination` builder (expects `Pagination` or `Option<Pagination>`)
197/// - `#[orm(limit)]` / `#[orm(offset)]` - Set LIMIT/OFFSET (expects `i64` or `Option<i64>`)
198/// - `#[orm(page)]` - Page-based pagination (expects `(page, per_page)` or `Option<(page, per_page)>`)
199/// - `#[orm(page(per_page = EXPR))]` - Page-based pagination from a page number (expects `i64`/`Option<i64>`)
200/// - `#[orm(raw)]` - Raw WHERE fragment (escape hatch)
201/// - `#[orm(and)]` / `#[orm(or)]` - Combine a `WhereExpr` (escape hatch)
202/// - `#[orm(skip)]` - Ignore this field
203#[proc_macro_derive(QueryParams, attributes(orm))]
204pub fn derive_query_params(input: TokenStream) -> TokenStream {
205    let input = parse_macro_input!(input as DeriveInput);
206    query_params::expand(input)
207        .unwrap_or_else(|e| e.to_compile_error())
208        .into()
209}