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};
31use tank_core::PrimaryKeyType;
32
33#[proc_macro_derive(Entity, attributes(tank))]
34pub fn derive_entity(input: TokenStream) -> TokenStream {
35    let table = decode_table(parse_macro_input!(input as ItemStruct));
36    let ident = &table.item.ident;
37    let name = &table.name;
38    let schema = &table.schema;
39    let metadata_and_filter = table
40        .columns
41        .iter()
42        .map(|metadata| {
43            let filter_passive = if let Some(ref filter_passive) = metadata.check_passive {
44                let field = &metadata.ident;
45                filter_passive(quote!(self.#field))
46            } else {
47                quote!(true)
48            };
49            (metadata, filter_passive)
50        })
51        .collect::<Vec<_>>();
52    let (from_row_factory, from_row) = from_row_trait(&table);
53    let primary_keys: Vec<_> = metadata_and_filter
54        .iter()
55        .enumerate()
56        .filter_map(|(i, (m, ..))| {
57            if matches!(
58                m.primary_key,
59                PrimaryKeyType::PrimaryKey | PrimaryKeyType::PartOfPrimaryKey
60            ) {
61                Some((i, m))
62            } else {
63                None
64            }
65        })
66        .collect();
67    let primary_key = primary_keys
68        .iter()
69        .map(|(_i, c)| c.ident.clone())
70        .map(|ident| quote!(self.#ident));
71    let primary_key_def = primary_keys.iter().map(|(i, _)| quote!(columns[#i]));
72    let unique_defs = &table
73        .unique
74        .iter()
75        .map(|v| {
76            if v.is_empty() {
77                quote!()
78            } else {
79                let i = v.iter();
80                quote!(vec![#(&columns[#i]),*].into_boxed_slice())
81            }
82        })
83        .collect::<Vec<_>>();
84    let unique_defs = quote!(vec![#(#unique_defs),*].into_boxed_slice());
85    let primary_key_types = primary_keys.iter().map(|(_, c)| c.ty.clone());
86    let column = column_trait(&table);
87    let label_value_and_filter = metadata_and_filter.iter().map(|(column, filter)| {
88        let name = &column.name;
89        let field = &column.ident;
90        quote!((#name.into(), ::tank::AsValue::as_value(self.#field.clone()), #filter))
91    });
92    let row_full = metadata_and_filter.iter().map(
93        |(ColumnMetadata { ident, .. }, _)| quote!(::tank::AsValue::as_value(self.#ident.clone())),
94    );
95    let columns = metadata_and_filter.iter().map(|(c, _)| {
96        let field = &c.ident;
97        encode_column_def(&c, quote!(#ident::#field))
98    });
99    let primary_key_condition = primary_keys.iter().enumerate().map(|(i, (_, c))| {
100        (
101            &c.ident,
102            Index::from(i),
103            Ident::new(&format!("pk{}", i), c.ident.span()),
104        )
105    });
106    let primary_key_condition_declaration = primary_key_condition
107        .clone()
108        .map(|(_, i, pk)| quote! { let #pk = primary_key.#i.to_owned(); })
109        .collect::<TokenStream2>();
110    let primary_key_condition_expression = primary_key_condition
111        .clone()
112        .map(|(field, _i, pk)| quote!(#ident::#field == # #pk))
113        .collect::<Punctuated<_, AndAnd>>();
114    quote! {
115        #from_row
116        #column
117        impl ::tank::Entity for #ident {
118            type PrimaryKey<'a> = (#(&'a #primary_key_types,)*);
119
120            fn table() -> &'static ::tank::TableRef {
121                static TABLE: ::tank::TableRef = ::tank::TableRef {
122                    name: #name,
123                    schema: #schema,
124                    alias: ::std::borrow::Cow::Borrowed(""),
125                };
126                &TABLE
127            }
128
129            fn columns() -> &'static [::tank::ColumnDef] {
130                static RESULT: ::std::sync::LazyLock<Box<[::tank::ColumnDef]>> =
131                    ::std::sync::LazyLock::new(|| vec![#(#columns),*].into_boxed_slice());
132                &RESULT
133            }
134
135            fn primary_key_def() -> impl ExactSizeIterator<Item = &'static ::tank::ColumnDef> {
136                static RESULT: ::std::sync::LazyLock<Box<[&::tank::ColumnDef]>> =
137                    ::std::sync::LazyLock::new(|| {
138                        let columns = #ident::columns();
139                        vec![#(&#primary_key_def),*].into_boxed_slice()
140                    });
141                RESULT.iter().copied()
142            }
143
144            fn primary_key<'a>(&'a self) -> Self::PrimaryKey<'a> {
145                (#(&#primary_key,)*)
146            }
147
148            fn unique_defs()
149            -> impl ExactSizeIterator<Item = impl ExactSizeIterator<Item = &'static ::tank::ColumnDef>> {
150                static RESULT: ::std::sync::LazyLock<Box<[Box<[&'static ::tank::ColumnDef]>]>> =
151                    ::std::sync::LazyLock::new(|| {
152                        let columns = #ident::columns();
153                        #unique_defs
154                    });
155                RESULT.iter().map(|v| v.iter().copied())
156            }
157
158            fn row_filtered(&self) -> Box<[(&'static str, ::tank::Value)]> {
159                [#(#label_value_and_filter),*]
160                    .into_iter()
161                    .filter_map(|(n, v, f)| if f { Some((n, v)) } else { None })
162                    .collect()
163            }
164
165            fn row_full(&self) -> ::tank::Row {
166                [#(#row_full),*].into()
167            }
168
169            fn from_row(row: ::tank::RowLabeled) -> ::tank::Result<Self> {
170                #from_row_factory::<Self>::from_row(row)
171            }
172
173            async fn create_table(
174                executor: &mut impl ::tank::Executor,
175                if_not_exists: bool,
176                create_schema: bool,
177            ) -> ::tank::Result<()> {
178                let mut query = String::with_capacity(2048);
179                if create_schema && !#schema.is_empty() {
180                    ::tank::SqlWriter::write_create_schema::<#ident>(
181                        &::tank::Driver::sql_writer(executor.driver()),
182                        &mut query,
183                        true,
184                    );
185                }
186                ::tank::SqlWriter::write_create_table::<#ident>(
187                    &::tank::Driver::sql_writer(executor.driver()),
188                    &mut query,
189                    if_not_exists,
190                );
191                // Remove the FutureExt::boxed wrapper once once https://github.com/rust-lang/rust/issues/100013 is fixed
192                ::tank::future::FutureExt::boxed(executor.execute(query))
193                    .await
194                    .map(|_| ())
195            }
196
197            async fn drop_table(
198                executor: &mut impl ::tank::Executor,
199                if_exists: bool,
200                drop_schema: bool,
201            ) -> ::tank::Result<()> {
202                let mut query = String::with_capacity(256);
203                ::tank::SqlWriter::write_drop_table::<#ident>(
204                    &::tank::Driver::sql_writer(executor.driver()),
205                    &mut query,
206                    if_exists,
207                );
208                if drop_schema && !#schema.is_empty() {
209                    ::tank::SqlWriter::write_drop_schema::<#ident>(
210                        &::tank::Driver::sql_writer(executor.driver()),
211                        &mut query,
212                        true,
213                    );
214                }
215                // Remove the FutureExt::boxed wrapper once https://github.com/rust-lang/rust/issues/100013 is fixed
216                ::tank::future::FutureExt::boxed(executor.execute(query))
217                    .await
218                    .map(|_| ())
219            }
220
221            fn insert_one(
222                executor: &mut impl ::tank::Executor,
223                entity: &impl ::tank::Entity,
224            ) -> impl ::std::future::Future<Output = ::tank::Result<::tank::RowsAffected>> + Send {
225                let mut query = String::with_capacity(128);
226                ::tank::SqlWriter::write_insert(
227                    &::tank::Driver::sql_writer(executor.driver()),
228                    &mut query,
229                    [entity],
230                    false,
231                );
232                executor.execute(query)
233            }
234
235            fn insert_many<'a, It>(
236                executor: &mut impl ::tank::Executor,
237                entities: It,
238            ) -> impl ::std::future::Future<Output = ::tank::Result<::tank::RowsAffected>> + Send
239            where
240                Self: 'a,
241                It: IntoIterator<Item = &'a Self> + Send,
242            {
243                executor.append(entities)
244            }
245
246            fn find_pk(
247                executor: &mut impl ::tank::Executor,
248                primary_key: &Self::PrimaryKey<'_>,
249            ) -> impl ::std::future::Future<Output = ::tank::Result<Option<Self>>> {
250                async move {
251                    #primary_key_condition_declaration
252                    let condition = ::tank::expr!(#primary_key_condition_expression);
253                    let stream = ::tank::DataSet::select(
254                        Self::table(),
255                        executor,
256                        Self::columns()
257                            .iter()
258                            .map(|c| &c.column_ref as &dyn ::tank::Expression),
259                        &condition,
260                        Some(1),
261                    );
262                    // Replace StreamExt::boxed wrapper with ::std::pin::pin! once https://github.com/rust-lang/rust/issues/100013 is fixed
263                    let mut stream = ::tank::stream::StreamExt::boxed(stream);
264                    ::tank::stream::StreamExt::next(&mut stream)
265                        .await
266                        .map(|v| v.and_then(Self::from_row))
267                        .transpose()
268                    }
269            }
270
271            fn find_many(
272                executor: &mut impl ::tank::Executor,
273                condition: &impl ::tank::Expression,
274                limit: Option<u32>,
275            ) -> impl ::tank::stream::Stream<Item = ::tank::Result<Self>> {
276                ::tank::stream::StreamExt::map(
277                    ::tank::DataSet::select(
278                        Self::table(),
279                        executor,
280                        Self::columns()
281                            .iter()
282                            .map(|c| &c.column_ref as &dyn ::tank::Expression),
283                        condition,
284                        limit,
285                    ),
286                    |result| result.and_then(Self::from_row),
287                )
288            }
289
290            fn delete_one(
291                executor: &mut impl ::tank::Executor,
292                primary_key: Self::PrimaryKey<'_>,
293            ) -> impl ::std::future::Future<Output = ::tank::Result<::tank::RowsAffected>> + Send
294            where
295                Self: Sized
296            {
297                #primary_key_condition_declaration
298                let condition = ::tank::expr!(#primary_key_condition_expression);
299                let mut query = String::with_capacity(128);
300                ::tank::SqlWriter::write_delete::<Self>(
301                    &::tank::Driver::sql_writer(executor.driver()),
302                    &mut query,
303                    &condition,
304                );
305                executor.execute(query)
306            }
307
308            fn delete_many(
309                executor: &mut impl ::tank::Executor,
310                condition: &impl ::tank::Expression,
311            ) -> impl ::std::future::Future<Output = ::tank::Result<::tank::RowsAffected>> + Send
312            where
313                Self: Sized
314            {
315                let mut query = String::with_capacity(128);
316                ::tank::SqlWriter::write_delete::<Self>(
317                    &::tank::Driver::sql_writer(executor.driver()),
318                    &mut query,
319                    condition,
320                );
321                executor.execute(query)
322            }
323        }
324    }
325    .into()
326}
327
328#[proc_macro]
329/// Build a typed join tree from a concise SQL-like syntax.
330///
331/// The grammar supports standard join variants (`JOIN`, `INNER JOIN`, `LEFT
332/// JOIN`, `LEFT OUTER JOIN`, `RIGHT JOIN`, `RIGHT OUTER JOIN`, `FULL OUTER
333/// JOIN`, `OUTER JOIN`, `CROSS`, `NATURAL JOIN`) plus nesting via parentheses
334/// and chaining multiple joins in sequence. Optional `ON <expr>` clauses are
335/// parsed into expressions using the same rules as [`expr!`].
336///
337/// Tables may be aliased by following them with an identifier (`MyTable MT
338/// JOIN Other ON MT.id == Other.other_id`). Parentheses group joins when
339/// building larger trees.
340///
341/// *Example*:
342/// ```rust
343/// let books = join!(Book JOIN Author ON Book::author == Author::id)
344///     .select(
345///         executor,
346///         cols!(Book::title, Author::name as author, Book::year),
347///         &true,
348///         None,
349///     )
350///     .and_then(|row| async { Books::from_row(row) })
351///     .try_collect::<HashSet<_>>()
352///     .await?;
353/// ```
354pub fn join(input: TokenStream) -> TokenStream {
355    let result = parse_macro_input!(input as JoinParsed);
356    result.0.into()
357}
358
359#[proc_macro]
360/// Parse a Rust expression into a typed SQL expression tree.
361///
362/// The macro accepts a subset of Rust syntax with additional sentinel tokens for SQL semantics:
363/// - `42`, `1.2`, `"Alpha"`, `true`, `NULL`, `[1, 2, 3]` literal values
364/// - `#value` variable evaluation
365/// - `RadioLog::signal_strength` column reference
366/// - `Operator::id == #some_uuid` comparison: `==`, `!=`, `>`, `>=`. `<`, `<=`
367/// - `!Operator::is_certified || RadioLog::signal_strength < -20` logical: `&&`, `||`, `!`
368/// - `(a + b) * (c - d)` math operations: `+`, `-`, `*`, `/`, `%`
369/// - `(flags >> 1) & 3` bitwise operations: `|`, `&`, `<<`, `>>`
370/// - `[1, 2, 3][0]` array or map indexing
371/// - `alpha == ? && beta > ?` prepared statement parameters
372/// - `col == NULL`, `col != NULL` null check, it becomes `IS NULL` / `IS NOT NULL`
373/// - `COUNT(*)`, `SUM(RadioLog::signal_strength)` function calls and aggregates
374/// - `1 as u128` type casting
375/// - `PI` identifiers
376/// - `value != "ab%" as LIKE` pattern matching, it becomes `value NOT LIKE 'ab%'`,
377///   it also supports `REGEXP` and `GLOB` (actual supports depends on the driver)
378/// - `-(-PI) + 2 * (5 % (2 + 1)) == 7 && !(4 < 2)` combination of the previous
379///
380/// Parentheses obey standard Rust precedence.
381/// Empty invocation (`expr!()`) yields `false`.
382/// Ultimately, the drivers decide if and how these expressions are translated into the specific query language.
383///
384/// *Examples:*
385/// ```rust
386/// use tank::expr;
387/// let condition = expr!(User::age > 18 && User::active == true);
388/// let rust_articles = expr!(Post::title == "Rust%" as LIKE);
389/// let first_user = expr!(CAST(User::active as i32) == 1);
390/// ```
391pub fn expr(input: TokenStream) -> TokenStream {
392    let mut input: TokenStream = flag_evaluated(input.into()).into();
393    if input.is_empty() {
394        input = quote!(false).into();
395    }
396    let expr = parse_macro_input!(input as Expr);
397    let parsed = decode_expression(&expr);
398    quote!(#parsed).into()
399}
400
401#[proc_macro]
402/// Build a slice of column expressions (optionally ordered) suitable for a `SELECT` projection.
403/// Each comma separated item becomes either a expression (parsed via [`expr!`]) or a ordered expression when followed by `ASC` or `DESC`.
404///
405/// Returns `&[&dyn Expression]` allowing direct passing to APIs expecting a
406/// heterogeneous list of column expressions.
407///
408/// *Example*:
409/// ```rust
410/// use tank::{cols, DataSet};
411/// let names_stream = User::table().select(connection, cols!(User::id, User::name ASC), &true, Some(500));
412/// let products_stream = Product::table().select(
413///     executor,
414///     cols!(COUNT(*), AVG(Product::price)),
415///     &true,
416///     None
417/// );
418/// ```
419pub fn cols(input: TokenStream) -> TokenStream {
420    let input = flag_evaluated(input.into());
421    let Ok(ColList { cols: items }) = parse2(input) else {
422        panic!("Could not parse the columns");
423    };
424    let generated = items.iter().map(|item| {
425        let expr = &item.expr;
426        match &item.order {
427            Some(order) => {
428                quote! {
429                    ::tank::Ordered {
430                        order: #order,
431                        expression: ::tank::expr!(#expr),
432                    }
433                }
434            }
435            None => {
436                quote! { ::tank::expr!(#expr) }
437            }
438        }
439    });
440
441    TokenStream::from(quote! {
442        &[ #( &#generated as &dyn ::tank::Expression ),* ]
443    })
444}