tank_macros/
lib.rs

1mod cols;
2mod column_trait;
3mod decode_column;
4mod decode_expression;
5mod decode_join;
6mod decode_table;
7mod encode_column_def;
8mod encode_column_ref;
9mod frag_evaluated;
10mod from_row_trait;
11
12use crate::{
13    cols::ColList,
14    decode_column::ColumnMetadata,
15    decode_table::{TableMetadata, decode_table},
16    encode_column_def::encode_column_def,
17    from_row_trait::from_row_trait,
18};
19use column_trait::column_trait;
20use decode_column::decode_column;
21use decode_expression::decode_expression;
22use decode_join::JoinParsed;
23use frag_evaluated::flag_evaluated;
24use proc_macro::TokenStream;
25use proc_macro2::TokenStream as TokenStream2;
26use quote::quote;
27use syn::{
28    Expr, Ident, Index, ItemStruct, parse_macro_input, parse2, punctuated::Punctuated,
29    token::AndAnd,
30};
31
32#[proc_macro_derive(Entity, attributes(tank))]
33pub fn derive_entity(input: TokenStream) -> TokenStream {
34    let table = decode_table(parse_macro_input!(input as ItemStruct));
35    let ident = &table.item.ident;
36    let name = &table.name;
37    let schema = &table.schema;
38    let metadata_and_filter = table
39        .columns
40        .iter()
41        .map(|metadata| {
42            let filter_passive = if let Some(ref filter_passive) = metadata.check_passive {
43                let field = &metadata.ident;
44                filter_passive(quote!(self.#field))
45            } else {
46                quote!(true)
47            };
48            (metadata, filter_passive)
49        })
50        .collect::<Vec<_>>();
51    let (from_row_factory, from_row) = from_row_trait(&table);
52    let primary_key_cols = table.primary_key.iter().map(|i| &table.columns[*i]);
53    let primary_key = primary_key_cols.clone().map(|col| {
54        let ident = &col.ident;
55        quote!(self.#ident)
56    });
57    let primary_keys_def = table.primary_key.iter().map(|i| quote!(&columns[#i]));
58    let unique_defs = &table
59        .unique
60        .iter()
61        .map(|v| {
62            if v.is_empty() {
63                quote!()
64            } else {
65                let i = v.iter();
66                quote!(vec![#(&columns[#i]),*].into_boxed_slice())
67            }
68        })
69        .collect::<Vec<_>>();
70    let unique_defs = quote!(vec![#(#unique_defs),*].into_boxed_slice());
71    let primary_key_types = primary_key_cols.clone().map(|col| col.ty.clone());
72    let (column_trait, column) = column_trait(&table);
73    let label_value_and_filter = metadata_and_filter.iter().map(|(column, filter)| {
74        let name = &column.name;
75        let field = &column.ident;
76        quote!((#name.into(), ::tank::AsValue::as_value(self.#field.clone()), #filter))
77    });
78    let row_full = metadata_and_filter.iter().map(
79        |(ColumnMetadata { ident, .. }, _)| quote!(::tank::AsValue::as_value(self.#ident.clone())),
80    );
81    let columns = metadata_and_filter.iter().map(|(c, _)| {
82        let field = &c.ident;
83        encode_column_def(&c, quote!(<#ident as #column_trait>::#field))
84    });
85    let primary_key_condition = table.primary_key.iter().enumerate().map(|(i, pki)| {
86        let ident = table.columns[*pki].ident.clone();
87        let span = ident.span();
88        (ident, Ident::new(&format!("pk{}", i), span))
89    });
90    let primary_key_condition_declaration = primary_key_condition
91        .clone()
92        .enumerate()
93        .clone()
94        .map(|(i, (_, pk))| {
95            let i = Index::from(i);
96            quote! { let #pk = primary_key.#i.to_owned(); }
97        })
98        .collect::<TokenStream2>();
99    let primary_key_condition_expression = primary_key_condition
100        .clone()
101        .map(|(field, pk)| quote!(#ident::#field == # #pk))
102        .collect::<Punctuated<_, AndAnd>>();
103    quote! {
104        #from_row
105        #column
106        impl ::tank::Entity for #ident {
107            type PrimaryKey<'a> = (#(&'a #primary_key_types,)*);
108
109            fn table() -> &'static ::tank::TableRef {
110                static TABLE: ::tank::TableRef = ::tank::TableRef {
111                    name: ::std::borrow::Cow::Borrowed(#name),
112                    schema: ::std::borrow::Cow::Borrowed(#schema),
113                    alias: ::std::borrow::Cow::Borrowed(""),
114                };
115                &TABLE
116            }
117
118            fn columns() -> &'static [::tank::ColumnDef] {
119                static RESULT: ::std::sync::LazyLock<Box<[::tank::ColumnDef]>> =
120                    ::std::sync::LazyLock::new(|| vec![#(#columns),*].into_boxed_slice());
121                &RESULT
122            }
123
124            fn primary_key_def() ->  &'static [&'static ::tank::ColumnDef] {
125                static RESULT: ::std::sync::LazyLock<Box<[&'static ::tank::ColumnDef]>> =
126                    ::std::sync::LazyLock::new(|| {
127                        let columns = <#ident as ::tank::Entity>::columns();
128                        vec![#(#primary_keys_def),*].into_boxed_slice()
129                    });
130                &RESULT
131            }
132
133            fn primary_key<'a>(&'a self) -> Self::PrimaryKey<'a> {
134                (#(&#primary_key,)*)
135            }
136
137            fn unique_defs()
138            -> impl ExactSizeIterator<Item = impl ExactSizeIterator<Item = &'static ::tank::ColumnDef>> {
139                static RESULT: ::std::sync::LazyLock<Box<[Box<[&'static ::tank::ColumnDef]>]>> =
140                    ::std::sync::LazyLock::new(|| {
141                        let columns = #ident::columns();
142                        #unique_defs
143                    });
144                RESULT.iter().map(|v| v.iter().copied())
145            }
146
147            fn row_filtered(&self) -> Box<[(&'static str, ::tank::Value)]> {
148                [#(#label_value_and_filter),*]
149                    .into_iter()
150                    .filter_map(|(n, v, f)| if f { Some((n, v)) } else { None })
151                    .collect()
152            }
153
154            fn row_full(&self) -> ::tank::Row {
155                [#(#row_full),*].into()
156            }
157
158            fn from_row(row: ::tank::RowLabeled) -> ::tank::Result<Self> {
159                #from_row_factory::<Self>::from_row(row)
160            }
161
162            fn find_pk(
163                executor: &mut impl ::tank::Executor,
164                primary_key: &Self::PrimaryKey<'_>,
165            ) -> impl ::std::future::Future<Output = ::tank::Result<Option<Self>>> {
166                async move {
167                    #primary_key_condition_declaration
168                    let condition = ::tank::expr!(#primary_key_condition_expression);
169                    let query = ::tank::QueryBuilder::new()
170                        .select(Self::columns())
171                        .from(Self::table())
172                        .where_condition(condition)
173                        .limit(Some(1))
174                        .build(&executor.driver());
175                    // Replace StreamExt::boxed wrapper with ::std::pin::pin! once https://github.com/rust-lang/rust/issues/100013 is fixed
176                    let mut stream = ::tank::stream::StreamExt::boxed(executor.fetch(query));
177                    ::tank::stream::StreamExt::next(&mut stream)
178                        .await
179                        .map(|v| v.and_then(Self::from_row))
180                        .transpose()
181                    }
182            }
183
184            fn delete_one(
185                executor: &mut impl ::tank::Executor,
186                primary_key: Self::PrimaryKey<'_>,
187            ) -> impl ::std::future::Future<Output = ::tank::Result<::tank::RowsAffected>> + Send
188            where
189                Self: Sized
190            {
191                #primary_key_condition_declaration
192                let condition = ::tank::expr!(#primary_key_condition_expression);
193                let mut query = ::tank::DynQuery::with_capacity(128);
194                ::tank::SqlWriter::write_delete::<Self>(
195                    &::tank::Driver::sql_writer(&executor.driver()),
196                    &mut query,
197                    &condition,
198                );
199                executor.execute(query)
200            }
201        }
202    }
203    .into()
204}
205
206#[proc_macro]
207/// Build a typed join tree from a concise SQL-like syntax.
208///
209/// The grammar supports standard join variants (`JOIN`, `INNER JOIN`, `LEFT
210/// JOIN`, `LEFT OUTER JOIN`, `RIGHT JOIN`, `RIGHT OUTER JOIN`, `FULL OUTER
211/// JOIN`, `OUTER JOIN`, `CROSS`, `NATURAL JOIN`) plus nesting via parentheses
212/// and chaining multiple joins in sequence. Optional `ON <expr>` clauses are
213/// parsed into expressions using the same rules as [`expr!`].
214///
215/// Tables may be aliased by following them with an identifier (`MyTable MT
216/// JOIN Other ON MT.id == Other.other_id`). Parentheses group joins when
217/// building larger trees.
218///
219/// *Example*:
220/// ```ignore
221/// let books = join!(Book JOIN Author ON Book::author == Author::id)
222///     .select(
223///         executor,
224///         cols!(Book::title, Author::name as author, Book::year),
225///         &true,
226///         None,
227///     )
228///     .and_then(|row| async { Books::from_row(row) })
229///     .try_collect::<HashSet<_>>()
230///     .await?;
231/// ```
232pub fn join(input: TokenStream) -> TokenStream {
233    let result = parse_macro_input!(input as JoinParsed);
234    result.0.into()
235}
236
237#[proc_macro]
238/// Parse a Rust expression into a typed SQL expression tree.
239///
240/// The macro accepts a subset of Rust syntax with additional sentinel tokens for SQL semantics:
241/// - `42`, `1.2`, `"Alpha"`, `true`, `NULL`, `[1, 2, 3]` literal values
242/// - `#value` variable evaluation
243/// - `RadioLog::signal_strength` column reference
244/// - `Operator::id == #some_uuid` comparison: `==`, `!=`, `>`, `>=`. `<`, `<=`
245/// - `!Operator::is_certified || RadioLog::signal_strength < -20` logical: `&&`, `||`, `!`
246/// - `(a + b) * (c - d)` math operations: `+`, `-`, `*`, `/`, `%`
247/// - `(flags >> 1) & 3` bitwise operations: `|`, `&`, `<<`, `>>`
248/// - `[1, 2, 3][0]` array or map indexing
249/// - `alpha == ? && beta > ?` prepared statement parameters
250/// - `col == NULL`, `col != NULL` null check, it becomes `IS NULL` / `IS NOT NULL`
251/// - `COUNT(*)`, `SUM(RadioLog::signal_strength)` function calls and aggregates
252/// - `1 as u128` type casting
253/// - `PI` identifiers
254/// - `value != "ab%" as LIKE` pattern matching, it becomes `value NOT LIKE 'ab%'`,
255///   it also supports `REGEXP` and `GLOB` (actual supports depends on the driver)
256/// - `-(-PI) + 2 * (5 % (2 + 1)) == 7 && !(4 < 2)` combination of the previous
257///
258/// Parentheses obey standard Rust precedence.
259/// Empty invocation (`expr!()`) yields `false`.
260/// Ultimately, the drivers decide if and how these expressions are translated into the specific query language.
261///
262/// *Examples:*
263/// ```ignore
264/// use tank::expr;
265/// let condition = expr!(User::age > 18 && User::active == true);
266/// let rust_articles = expr!(Post::title == "Rust%" as LIKE);
267/// let first_user = expr!(CAST(User::active as i32) == 1);
268/// ```
269pub fn expr(input: TokenStream) -> TokenStream {
270    let mut input: TokenStream = flag_evaluated(input.into()).into();
271    if input.is_empty() {
272        input = quote!(false).into();
273    }
274    let expr = parse_macro_input!(input as Expr);
275    let parsed = decode_expression(&expr);
276    quote!(#parsed).into()
277}
278
279#[proc_macro]
280/// Build a slice of column expressions (optionally ordered) suitable for a `SELECT` projection.
281/// Each comma separated item becomes either an expression (parsed via [`expr!`]) or an ordered expression when followed by `ASC` or `DESC`.
282///
283/// Returns `&[&dyn Expression]` allowing direct passing to APIs expecting a
284/// heterogeneous list of column expressions.
285///
286/// *Example*:
287/// ```ignore
288/// use tank::{cols, DataSet};
289/// let names_stream = User::table().select(connection, cols!(User::id, User::name ASC), &true, Some(500));
290/// let products_stream = Product::table().select(
291///     executor,
292///     cols!(COUNT(*), AVG(Product::price)),
293///     &true,
294///     None
295/// );
296/// ```
297pub fn cols(input: TokenStream) -> TokenStream {
298    let input = flag_evaluated(input.into());
299    let Ok(ColList { cols: items }) = parse2(input) else {
300        panic!("Could not parse the columns");
301    };
302    let generated = items.iter().map(|item| {
303        let expr = &item.expr;
304        match &item.order {
305            Some(order) => {
306                quote! {
307                    ::tank::Ordered {
308                        order: #order,
309                        expression: ::tank::expr!(#expr),
310                    }
311                }
312            }
313            None => {
314                quote! { ::tank::expr!(#expr) }
315            }
316        }
317    });
318
319    TokenStream::from(quote! {
320        &[ #( &#generated as &dyn ::tank::Expression ),* ]
321    })
322}