benzina_derive/lib.rs
1use proc_macro2::Span;
2use quote::ToTokens;
3use syn::{
4 DeriveInput, Ident, Path, PathArguments, PathSegment, parse_macro_input,
5 punctuated::Punctuated, token::PathSep,
6};
7
8use self::enum_derive::Enum;
9use crate::join::Join;
10
11mod enum_derive;
12mod join;
13mod rename_rule;
14
15/// Derive [`FromSql`] and [`ToSql`] for a Rust enum.
16/// Represents a PostgreSQL enum as a Rust enum.
17///
18/// ## Example
19///
20/// ### migration
21///
22/// ```sql
23/// CREATE TYPE animal AS ENUM (
24/// 'chicken',
25/// 'duck',
26/// 'oca',
27/// 'rabbit
28/// );
29/// ```
30///
31/// ### Rust enum
32///
33/// ```rust
34/// # use benzina_derive as benzina;
35/// # fn main() {}
36///
37/// #[derive(Debug, Copy, Clone, benzina::Enum)]
38/// #[benzina(
39/// sql_type = crate::schema::sql_types::Animal,
40/// rename_all = "snake_case"
41/// )]
42/// # #[benzina(crate = fake_benzina)]
43/// pub enum Animal {
44/// Chicken,
45/// Duck,
46/// #[benzina(rename = "oca")]
47/// Goose,
48/// Rabbit,
49/// }
50///
51/// pub mod schema {
52/// // @generated automatically by Diesel CLI.
53///
54/// pub mod sql_types {
55/// #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
56/// #[diesel(postgres_type(name = "animal"))]
57/// pub struct Animal;
58/// }
59/// }
60/// #
61/// # mod fake_benzina {
62/// # pub mod __private {
63/// # pub use std;
64/// # pub use diesel;
65/// # }
66/// # }
67/// ```
68///
69/// ## Enums with variant-specific data in separate JSONB column
70///
71/// You can also use `benzina::Enum` for enums where each variant holds
72/// associated data. This is useful when you have a PostgreSQL ENUM for the
73/// discriminator and a JSONB column for the variant-specific payload.
74///
75/// ### migration
76///
77/// ```sql
78/// CREATE TYPE animal AS ENUM ('chicken', 'duck', 'oca', 'rabbit');
79///
80/// CREATE TABLE pets (
81/// id SERIAL PRIMARY KEY,
82/// name TEXT NOT NULL,
83/// animal animal NOT NULL,
84/// animal_data JSONB NOT NULL
85/// );
86/// ```
87///
88/// ### Rust enum
89///
90/// ```rust
91/// # use benzina_derive as benzina;
92/// # fn main() {}
93/// use diesel::pg::Pg;
94/// use diesel::{Identifiable, Insertable, Queryable, Selectable};
95/// use serde::{Deserialize, Serialize};
96///
97/// #[derive(Debug, Queryable, Identifiable, Insertable, Selectable)]
98/// #[diesel(table_name = schema::pets, check_for_backend(Pg))]
99/// pub struct Pet {
100/// pub id: i32,
101/// pub name: String,
102/// #[diesel(embed)]
103/// pub animal: Animal,
104/// }
105///
106/// #[derive(Debug, Clone, benzina::Enum)]
107/// #[benzina(
108/// sql_type = schema::sql_types::Animal,
109/// rename_all = "snake_case",
110/// table = schema::pets,
111/// column = animal,
112/// data_column = animal_data
113/// )]
114/// # #[benzina(crate = fake_benzina)]
115/// pub enum Animal {
116/// Chicken(ChickenData),
117/// Duck(DuckData),
118/// #[benzina(rename = "oca")]
119/// Goose(GooseData),
120/// Rabbit(RabbitData),
121/// }
122///
123/// #[derive(Debug, Clone, Serialize, Deserialize)]
124/// pub struct ChickenData {
125/// pub likes_cuddles: bool,
126/// pub breed: String,
127/// }
128///
129/// #[derive(Debug, Clone, Serialize, Deserialize)]
130/// pub struct DuckData {
131/// pub favorite_treat: String,
132/// pub feather_color: String,
133/// }
134///
135/// #[derive(Debug, Clone, Serialize, Deserialize)]
136/// pub struct GooseData {
137/// pub weight_kg: f64,
138/// pub honks_at_strangers: bool,
139/// }
140///
141/// #[derive(Debug, Clone, Serialize, Deserialize)]
142/// pub struct RabbitData {
143/// pub fur_color: String,
144/// pub litter_trained: bool,
145/// }
146///
147/// pub mod schema {
148/// // @generated automatically by Diesel CLI.
149///
150/// pub mod sql_types {
151/// #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
152/// #[diesel(postgres_type(name = "animal"))]
153/// pub struct Animal;
154/// }
155///
156/// diesel::table! {
157/// use diesel::sql_types::*;
158/// use super::sql_types::Animal;
159///
160/// pets (id) {
161/// id -> Int4,
162/// name -> Text,
163/// animal -> Animal,
164/// animal_data -> Jsonb,
165/// }
166/// }
167/// }
168/// #
169/// # mod fake_benzina {
170/// # pub mod __private {
171/// # pub use std;
172/// # pub use diesel;
173/// #
174/// # pub mod json {
175/// # use diesel::{
176/// # deserialize::{FromSql, FromSqlRow},
177/// # expression::AsExpression,
178/// # pg::{Pg, PgValue},
179/// # serialize::ToSql,
180/// # sql_types,
181/// # };
182/// # use serde::{Deserialize, Serialize};
183/// #
184/// # #[derive(Debug, FromSqlRow, AsExpression)]
185/// # #[diesel(sql_type = sql_types::Jsonb)]
186/// # pub struct RawJsonb;
187/// #
188/// # impl RawJsonb {
189/// # pub const EMPTY: Self = Self;
190/// #
191/// # pub fn serialize(value: &impl Serialize) -> diesel::deserialize::Result<Self> {
192/// # unimplemented!()
193/// # }
194/// #
195/// # pub fn deserialize<T: for<'a> Deserialize<'a>>(&self) -> diesel::deserialize::Result<T> {
196/// # unimplemented!()
197/// # }
198/// # }
199/// #
200/// # impl FromSql<sql_types::Jsonb, Pg> for RawJsonb {
201/// # fn from_sql(value: PgValue) -> diesel::deserialize::Result<Self> {
202/// # unimplemented!()
203/// # }
204/// # }
205/// #
206/// # impl ToSql<sql_types::Jsonb, Pg> for RawJsonb {
207/// # fn to_sql(&self, out: &mut diesel::serialize::Output<Pg>) -> diesel::serialize::Result {
208/// # unimplemented!()
209/// # }
210/// # }
211/// # }
212/// # }
213/// # }
214/// ```
215///
216/// [`FromSql`]: https://docs.rs/diesel/latest/diesel/deserialize/trait.FromSql.html
217/// [`ToSql`]: https://docs.rs/diesel/latest/diesel/serialize/trait.ToSql.html
218#[proc_macro_derive(Enum, attributes(benzina))]
219pub fn benzina_enum_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
220 let input = parse_macro_input!(input as DeriveInput);
221
222 Enum::parse(input)
223 .map_or_else(syn::Error::into_compile_error, ToTokens::into_token_stream)
224 .into()
225}
226
227/// Convert the output of a query containing joins into a properly nested structure.
228///
229/// <div class="warning">
230/// This macro is still in the experimental stage and may contain
231/// bugs and unhelpful error diagnostics.
232/// </div>
233///
234/// Enable the `rustc-hash` feature to use a faster but non-DOS-resistant hasher for
235/// the internal maps.
236///
237/// ## Example
238///
239/// ```rust,compile_fail
240/// # fn main() {}
241///
242/// use diesel::{
243/// Identifiable, QueryDsl, QueryResult, Queryable, RunQueryDsl, Selectable, SelectableHelper,
244/// pg::{Pg, PgConnection},
245/// };
246///
247/// #[derive(Debug, Queryable, Identifiable, Selectable)]
248/// #[diesel(table_name = users, check_for_backend(Pg))]
249/// pub struct User {
250/// pub id: i32,
251/// pub name: String,
252/// }
253///
254/// #[derive(Debug)]
255/// pub struct UserWithPosts {
256/// pub user: User,
257/// pub posts: Vec<PostFromUser>,
258/// }
259///
260/// #[derive(Debug, Queryable, Identifiable, Selectable)]
261/// #[diesel(table_name = topics, check_for_backend(Pg))]
262/// pub struct Topic {
263/// pub id: i32,
264/// pub name: String,
265/// }
266///
267/// #[derive(Debug, Queryable, Identifiable, Selectable)]
268/// #[diesel(table_name = posts, check_for_backend(Pg))]
269/// pub struct Post {
270/// pub id: i32,
271/// pub user_id: i32,
272/// pub topic_id: i32,
273/// pub message: String,
274/// }
275///
276/// #[derive(Debug)]
277/// pub struct PostFromUser {
278/// pub post: Post,
279/// pub topic: Topic,
280/// pub comments: Vec<CommentFromPost>,
281/// }
282///
283/// #[derive(Debug, Queryable, Identifiable, Selectable)]
284/// #[diesel(table_name = comments, check_for_backend(Pg))]
285/// pub struct Comment {
286/// pub id: i32,
287/// pub post_id: i32,
288/// pub user_id: i32,
289/// pub message: String,
290/// }
291///
292/// #[derive(Debug)]
293/// pub struct CommentFromPost {
294/// pub comment: Comment,
295/// pub user: User,
296/// }
297///
298/// impl UserWithPosts {
299/// pub fn get_by_id(conn: &mut PgConnection, user_id: i32) -> QueryResult<Vec<Self>> {
300/// let (users1, users2) = diesel::alias!(users as users1, users as users2);
301///
302/// let records = users1
303/// .find(user_id)
304/// .left_join(
305/// posts::table
306/// .left_join(topics::table)
307/// .left_join(comments::table.left_join(users2)),
308/// )
309/// .select((
310/// users1.fields(<User as Selectable<Pg>>::construct_selection()),
311/// Option::<Post>::as_select(),
312/// Option::<Topic>::as_select(),
313/// Option::<Comment>::as_select(),
314/// users2.fields(<Option<User> as Selectable<Pg>>::construct_selection()),
315/// ))
316/// .get_results::<(
317/// User,
318/// Option<Post>,
319/// Option<Topic>,
320/// Option<Comment>,
321/// Option<User>,
322/// )>(conn)?;
323///
324/// let joined = benzina::join! {
325/// records,
326/// Vec<UserWithPosts {
327/// user: One<0>,
328/// posts: Vec0<PostFromUser {
329/// post: One<1>,
330/// topic: AssumeOne<2>,
331/// comments: Vec0<CommentFromPost {
332/// comment: One<3>,
333/// user: AssumeOne<4>,
334/// }>,
335/// }>,
336/// }>,
337/// };
338/// Ok(joined)
339/// }
340/// }
341///
342/// diesel::table! {
343/// users {
344/// id -> Integer,
345/// name -> Text,
346/// }
347/// }
348///
349/// diesel::table! {
350/// topics {
351/// id -> Integer,
352/// name -> Text,
353/// }
354/// }
355///
356/// diesel::table! {
357/// posts {
358/// id -> Integer,
359/// user_id -> Integer,
360/// topic_id -> Integer,
361/// message -> Text,
362/// }
363/// }
364///
365/// diesel::table! {
366/// comments {
367/// id -> Integer,
368/// post_id -> Integer,
369/// user_id -> Integer,
370/// message -> Text,
371/// }
372/// }
373///
374/// diesel::joinable!(posts -> users (user_id));
375/// diesel::joinable!(posts -> topics (topic_id));
376/// diesel::joinable!(comments -> posts (post_id));
377/// diesel::joinable!(comments -> users (user_id));
378///
379/// diesel::allow_tables_to_appear_in_same_query!(users, topics, posts, comments);
380/// ```
381#[proc_macro]
382pub fn join(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
383 let input = parse_macro_input!(input as Join);
384 input.into_token_stream().into()
385}
386
387#[expect(clippy::ref_option, reason = "it's easier to use")]
388fn crate_name(crate_name: &Option<Path>) -> Path {
389 crate_name.clone().unwrap_or_else(|| {
390 let mut segments = Punctuated::new();
391 segments.push(PathSegment {
392 ident: Ident::new("benzina", Span::call_site()),
393 arguments: PathArguments::None,
394 });
395 Path {
396 leading_colon: Some(PathSep {
397 spans: [Span::call_site(); 2],
398 }),
399 segments,
400 }
401 })
402}