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 pg_composite;
13mod pg_enum;
14mod query_params;
15mod sql_ident;
16mod update_model;
17
18/// Derive `FromRow` trait for a struct.
19///
20/// # Example
21///
22/// ```ignore
23/// use pgorm::FromRow;
24///
25/// #[derive(FromRow)]
26/// struct User {
27/// id: i64,
28/// username: String,
29/// #[orm(column = "email_address")]
30/// email: Option<String>,
31/// }
32/// ```
33///
34/// # Attributes
35///
36/// - `#[orm(column = "name")]` - Map field to a different column name
37#[proc_macro_derive(FromRow, attributes(orm))]
38pub fn derive_from_row(input: TokenStream) -> TokenStream {
39 let input = parse_macro_input!(input as DeriveInput);
40 from_row::expand(input)
41 .unwrap_or_else(|e| e.to_compile_error())
42 .into()
43}
44
45/// Derive `Model` metadata for a struct.
46///
47/// # Example
48///
49/// ```ignore
50/// use pgorm::Model;
51///
52/// #[derive(Model)]
53/// #[orm(table = "users")]
54/// struct User {
55/// #[orm(id)]
56/// user_id: i64,
57/// username: String,
58/// email: Option<String>,
59/// }
60/// ```
61///
62/// # Generated
63///
64/// - `TABLE: &'static str` - Table name
65/// - `COL_*: &'static str` - Column name constants
66/// - `SELECT_LIST: &'static str` - Comma-separated column list
67/// - `fn select_list_as(alias: &str) -> String` - Aliased column list for JOINs
68///
69/// # Attributes
70///
71/// Struct-level:
72///
73/// - `#[orm(table = "name")]` - Specify table name (required)
74/// - `#[orm(join(table = "...", on = "...", type = "inner|left|right|full|cross"))]` - Add JOINs (optional, repeatable)
75/// - `#[orm(has_many(ChildType, foreign_key = "...", as = "..."))]` - Generate select_has_many helpers (optional, repeatable)
76/// - `#[orm(belongs_to(ParentType, foreign_key = "...", as = "..."))]` - Generate select_belongs_to helpers (optional, repeatable)
77///
78/// Field-level:
79///
80/// - `#[orm(id)]` - Mark field as primary key
81/// - `#[orm(column = "name")]` - Map field to a different column name
82/// - `#[orm(table = "name")]` - Mark field as coming from a joined table (for view/join models)
83#[proc_macro_derive(Model, attributes(orm))]
84pub fn derive_model(input: TokenStream) -> TokenStream {
85 let input = parse_macro_input!(input as DeriveInput);
86 model::expand(input)
87 .unwrap_or_else(|e| e.to_compile_error())
88 .into()
89}
90
91/// Derive `ViewModel` metadata for a struct.
92///
93/// This is an alias of `Model` intended to express that the type is a read/view model
94/// (optionally including JOINs), while write models are derived separately.
95#[proc_macro_derive(ViewModel, attributes(orm))]
96pub fn derive_view_model(input: TokenStream) -> TokenStream {
97 let input = parse_macro_input!(input as DeriveInput);
98 model::expand(input)
99 .unwrap_or_else(|e| e.to_compile_error())
100 .into()
101}
102
103/// Derive `InsertModel` helpers for inserting into a table.
104///
105/// # Attributes
106///
107/// Struct-level:
108///
109/// - `#[orm(table = "name")]` - Specify table name (required)
110/// - `#[orm(returning = "TypePath")]` - Enable `insert_returning` helpers (optional)
111/// - Conflict handling (Postgres `ON CONFLICT`):
112/// - `#[orm(conflict_target = "col1,col2")]` - conflict target columns (optional)
113/// - `#[orm(conflict_constraint = "constraint_name")]` - conflict constraint (optional)
114/// - `#[orm(conflict_update = "col1,col2")]` - columns to update on conflict (optional)
115/// - Multi-table write graphs (advanced): function-style attrs like `#[orm(has_many(...))]`,
116/// `#[orm(belongs_to(...))]`, `#[orm(before_insert(...))]`. See `docs/design/multi-table-writes-final.md`.
117///
118/// Field-level:
119///
120/// - `#[orm(id)]` - Mark field as primary key (optional)
121/// - `#[orm(skip_insert)]` - Never include this field in INSERT
122/// - `#[orm(default)]` - Use SQL `DEFAULT` for this field
123/// - `#[orm(auto_now_add)]` - Use `NOW()` for this field on insert
124/// - `#[orm(column = "name")]` / `#[orm(table = "name")]` - Override column/table mapping (optional)
125#[proc_macro_derive(InsertModel, attributes(orm))]
126pub fn derive_insert_model(input: TokenStream) -> TokenStream {
127 let input = parse_macro_input!(input as DeriveInput);
128 insert_model::expand(input)
129 .unwrap_or_else(|e| e.to_compile_error())
130 .into()
131}
132
133/// Derive `UpdateModel` helpers for updating a table (patch-style).
134///
135/// # Attributes
136///
137/// Struct-level:
138///
139/// - `#[orm(table = "name")]` - Specify table name (required)
140/// - One of:
141/// - `#[orm(id_column = "id")]` - Explicit primary key column
142/// - `#[orm(model = "TypePath")]` - Derive primary key column from a `Model`
143/// - `#[orm(returning = "TypePath")]` where `TypePath::ID` exists
144/// - `#[orm(returning = "TypePath")]` - Enable `update_by_id_returning` helpers (optional)
145/// - Multi-table write graphs (advanced): see `docs/design/multi-table-writes-final.md`.
146///
147/// Field-level:
148///
149/// - `#[orm(skip_update)]` - Never include this field in UPDATE
150/// - `#[orm(default)]` - Use SQL `DEFAULT` for this field
151/// - `#[orm(auto_now)]` - Use `NOW()` for this field on update
152/// - `#[orm(column = "name")]` / `#[orm(table = "name")]` - Override column/table mapping (optional)
153#[proc_macro_derive(UpdateModel, attributes(orm))]
154pub fn derive_update_model(input: TokenStream) -> TokenStream {
155 let input = parse_macro_input!(input as DeriveInput);
156 update_model::expand(input)
157 .unwrap_or_else(|e| e.to_compile_error())
158 .into()
159}
160
161/// Derive `QueryParams` helpers for building dynamic queries from a params struct.
162///
163/// # Example
164///
165/// ```ignore
166/// use pgorm::QueryParams;
167///
168/// #[derive(QueryParams)]
169/// #[orm(model = "User")]
170/// struct UserSearchParams<'a> {
171/// #[orm(eq(UserQuery::COL_ID))]
172/// id: Option<i64>,
173/// #[orm(eq(UserQuery::COL_EMAIL))]
174/// email: Option<&'a str>,
175/// }
176///
177/// let q = UserSearchParams { id, email }.into_query()?;
178/// ```
179///
180/// # Attributes
181///
182/// Struct-level:
183/// - `#[orm(model = "TypePath")]` - The model type that provides `Model::query()`
184///
185/// Field-level:
186/// - `#[orm(eq(COL))]` - Equality filter (auto uses `eq_opt_str` for `&str`/`String`)
187/// - `#[orm(eq_str(COL))]` - Equality filter, forcing string conversion
188/// - `#[orm(eq_map(COL, map_fn))]` - Equality filter after mapping (e.g. parse)
189/// - `#[orm(map(map_fn))]` - Optional mapper (returns `Option<T>`; `None` means "skip filter")
190/// - `#[orm(ne(COL))]` / `#[orm(gt(COL))]` / `#[orm(gte(COL))]` / `#[orm(lt(COL))]` / `#[orm(lte(COL))]`
191/// - `#[orm(like(COL))]` / `#[orm(ilike(COL))]` / `#[orm(not_like(COL))]` / `#[orm(not_ilike(COL))]`
192/// - `#[orm(in_list(COL))]` / `#[orm(not_in(COL))]`
193/// - `#[orm(between(COL))]` / `#[orm(not_between(COL))]` (expects `(T, T)` or `Option<(T, T)>`)
194/// - `#[orm(is_null(COL))]` / `#[orm(is_not_null(COL))]` (expects `bool` or `Option<bool>`)
195/// - `#[orm(order_by)]` - Replace the `OrderBy` builder (expects `OrderBy` or `Option<OrderBy>`)
196/// - `#[orm(order_by_asc)]` / `#[orm(order_by_desc)]` - Add an ORDER BY column (expects a column ident or `Option<...>`)
197/// - `#[orm(order_by_raw)]` - Add a raw ORDER BY item (escape hatch)
198/// - `#[orm(paginate)]` - Replace the `Pagination` builder (expects `Pagination` or `Option<Pagination>`)
199/// - `#[orm(limit)]` / `#[orm(offset)]` - Set LIMIT/OFFSET (expects `i64` or `Option<i64>`)
200/// - `#[orm(page)]` - Page-based pagination (expects `(page, per_page)` or `Option<(page, per_page)>`)
201/// - `#[orm(page(per_page = EXPR))]` - Page-based pagination from a page number (expects `i64`/`Option<i64>`)
202/// - `#[orm(raw)]` - Raw WHERE fragment (escape hatch)
203/// - `#[orm(and)]` / `#[orm(or)]` - Combine a `WhereExpr` (escape hatch)
204/// - `#[orm(skip)]` - Ignore this field
205#[proc_macro_derive(QueryParams, attributes(orm))]
206pub fn derive_query_params(input: TokenStream) -> TokenStream {
207 let input = parse_macro_input!(input as DeriveInput);
208 query_params::expand(input)
209 .unwrap_or_else(|e| e.to_compile_error())
210 .into()
211}
212
213/// Derive `PgEnum` helpers to map a Rust enum to a PostgreSQL ENUM type.
214///
215/// # Example
216///
217/// ```ignore
218/// use pgorm::PgEnum;
219///
220/// #[derive(PgEnum, Debug, Clone, PartialEq)]
221/// #[orm(pg_type = "order_status")]
222/// pub enum OrderStatus {
223/// #[orm(rename = "pending")]
224/// Pending,
225/// Processing, // defaults to "processing" (snake_case)
226/// Shipped,
227/// Delivered,
228/// Cancelled,
229/// }
230/// ```
231///
232/// # Generated
233///
234/// - `impl ToSql for OrderStatus`
235/// - `impl<'a> FromSql<'a> for OrderStatus`
236/// - `impl PgType for OrderStatus` (returns `"{pg_type}[]"`)
237///
238/// # Attributes
239///
240/// Enum-level:
241/// - `#[orm(pg_type = "name")]` - PostgreSQL ENUM type name (required)
242///
243/// Variant-level:
244/// - `#[orm(rename = "name")]` - Override the SQL string for this variant (optional, defaults to snake_case)
245#[proc_macro_derive(PgEnum, attributes(orm))]
246pub fn derive_pg_enum(input: TokenStream) -> TokenStream {
247 let input = parse_macro_input!(input as DeriveInput);
248 pg_enum::expand(input)
249 .unwrap_or_else(|e| e.to_compile_error())
250 .into()
251}
252
253/// Derive `PgComposite` helpers to map a Rust struct to a PostgreSQL composite type.
254///
255/// # Example
256///
257/// ```ignore
258/// use pgorm::PgComposite;
259///
260/// #[derive(PgComposite, Debug, Clone)]
261/// #[orm(pg_type = "address")]
262/// pub struct Address {
263/// pub street: String,
264/// pub city: String,
265/// pub zip_code: String,
266/// pub country: String,
267/// }
268/// ```
269///
270/// # Generated
271///
272/// - `impl ToSql for Address`
273/// - `impl<'a> FromSql<'a> for Address`
274/// - `impl PgType for Address` (returns `"{pg_type}[]"`)
275///
276/// # Attributes
277///
278/// Struct-level:
279/// - `#[orm(pg_type = "name")]` - PostgreSQL composite type name (required)
280///
281/// # Limitations
282///
283/// - Only flat structs with named fields are supported.
284/// - Nested composite types are not supported.
285/// - All fields must implement `ToSql` and `FromSql`.
286#[proc_macro_derive(PgComposite, attributes(orm))]
287pub fn derive_pg_composite(input: TokenStream) -> TokenStream {
288 let input = parse_macro_input!(input as DeriveInput);
289 pg_composite::expand(input)
290 .unwrap_or_else(|e| e.to_compile_error())
291 .into()
292}