rust_query_macros/
lib.rs

1use dummy::{dummy_impl, from_expr};
2use heck::{ToSnekCase, ToUpperCamelCase};
3use multi::{SingleVersionTable, VersionedSchema};
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use syn::{Ident, ItemMod, ItemStruct};
7use table::define_all_tables;
8
9mod dummy;
10mod migrations;
11mod multi;
12mod parse;
13mod table;
14mod unique;
15
16/// Use this macro to define your schema.
17///
18/// ## Supported data types:
19/// - `i64` (sqlite `integer`)
20/// - `f64` (sqlite `real`)
21/// - `String` (sqlite `text`)
22/// - `Vec<u8>` (sqlite `blob`)
23/// - `bool` (sqlite `integer` with `CHECK "col" IN (0, 1)`)
24/// - Any table in the same schema (sqlite `integer` with foreign key constraint)
25/// - `Option<T>` where `T` is not an `Option` (sqlite nullable)
26///
27/// ## Unique constraints
28///
29/// To define a unique constraint on a column, you need to add an attribute to the table or field.
30///
31/// For example:
32/// ```
33/// #[rust_query::migration::schema(Schema)]
34/// #[version(0..=0)]
35/// pub mod vN {
36///     pub struct User {
37///         #[unique]
38///         pub email: String,
39///         #[unique]
40///         pub username: String,
41///     }
42/// }
43/// # fn main() {}
44/// ```
45/// This will create a single schema with a single table called `user` and two columns.
46/// The table will also have two unique contraints.
47/// Note that optional types are not allowed in unique constraints.
48///
49/// ## Multiple versions
50/// The macro must be applied to a module named `vN`. This is because the module
51/// is a template that is used to generate multiple modules called `v0`, `v1` etc.
52/// Each module corresponds to a schema version and contains the types to work with that schema.
53///
54/// Note in the previous example that the schema version range is `0..=0` so there is only a version 0.
55/// The generated code will have a structure like this:
56/// ```rust,ignore
57/// pub mod v0 {
58///     pub struct Schema;
59///     pub struct User{..};
60///     // a bunch of other stuff
61/// }
62/// ```
63///
64/// # Adding tables
65/// At some point you might want to add a new table.
66/// ```
67/// #[rust_query::migration::schema(Schema)]
68/// #[version(0..=1)]
69/// pub mod vN {
70///     pub struct User {
71///         #[unique]
72///         pub email: String,
73///         #[unique]
74///         pub username: String,
75///     }
76///     #[version(1..)] // <-- note that `Game`` has a version range
77///     pub struct Game {
78///         pub name: String,
79///         pub size: i64,
80///     }
81/// }
82/// # fn main() {}
83/// ```
84/// We now have two schema versions which generates two modules `v0` and `v1`.
85/// They look something like this:
86/// ```rust,ignore
87/// pub mod v0 {
88///     pub struct Schema;
89///     pub struct User{..};
90///     pub mod migrate {..}
91///     // a bunch of other stuff
92/// }
93/// pub mod v1 {
94///     pub struct Schema;
95///     pub struct User{..};
96///     pub struct Game{..};
97///     // a bunch of other stuff
98/// }
99/// ```
100///
101/// # Changing columns
102/// Changing columns is very similar to adding and removing structs.
103/// ```
104/// use rust_query::migration::{schema, Config};
105/// use rust_query::{Database, Lazy};
106/// #[schema(Schema)]
107/// #[version(0..=1)]
108/// pub mod vN {
109///     pub struct User {
110///         #[unique]
111///         pub email: String,
112///         #[unique]
113///         pub username: String,
114///         #[version(1..)] // <-- here
115///         pub score: i64,
116///     }
117/// }
118/// // In this case it is required to provide a value for each row that already exists.
119/// // This is done with the `v0::migrate::User` struct:
120/// pub fn migrate() -> Database<v1::Schema> {
121///     let m = Database::migrator(Config::open_in_memory()) // we use an in memory database for this test
122///         .expect("database version is before supported versions");
123///     let m = m.migrate(|txn| v0::migrate::Schema {
124///         user: txn.migrate_ok(|old: Lazy<v0::User>| v0::migrate::User {
125///             score: old.email.len() as i64 // use the email length as the new score
126///         }),
127///     });
128///     m.finish().expect("database version is after supported versions")
129/// }
130/// # fn main() {}
131/// ```
132/// The `migrate` function first creates an empty database if it does not exists.
133///
134/// # `#[from]` Attribute
135/// You can use this attribute when renaming or splitting a table.
136/// This will make it clear that data in the table should have the
137/// same row ids as the `from` table.
138///
139/// For example:
140///
141/// ```
142/// # use rust_query::migration::schema;
143/// # fn main() {}
144/// #[schema(Schema)]
145/// #[version(0..=1)]
146/// pub mod vN {
147///     #[version(..1)]
148///     pub struct User {
149///         pub name: String,
150///     }
151///     #[version(1..)]
152///     #[from(User)]
153///     pub struct Author {
154///         pub name: String,
155///     }
156///     pub struct Book {
157///         pub author: Author,
158///     }
159/// }
160/// ```
161/// In this example the `Book` table exists in both `v0` and `v1`,
162/// however `User` only exists in `v0` and `Author` only exist in `v1`.
163/// Note that the `pub author: Author` field only specifies the latest version
164/// of the table, it will use the `#[from]` attribute to find previous versions.
165///
166/// This will work correctly and will let you migrate data from `User` to `Author` with code like this:
167///
168/// ```rust
169/// # use rust_query::migration::{schema, Config};
170/// # use rust_query::{Database, Lazy};
171/// # fn main() {}
172/// # #[schema(Schema)]
173/// # #[version(0..=1)]
174/// # pub mod vN {
175/// #     #[version(..1)]
176/// #     pub struct User {
177/// #         pub name: String,
178/// #     }
179/// #     #[version(1..)]
180/// #     #[from(User)]
181/// #     pub struct Author {
182/// #         pub name: String,
183/// #     }
184/// #     pub struct Book {
185/// #         pub author: Author,
186/// #     }
187/// # }
188/// # pub fn migrate() -> Database<v1::Schema> {
189/// #     let m = Database::migrator(Config::open_in_memory()) // we use an in memory database for this test
190/// #         .expect("database version is before supported versions");
191/// let m = m.migrate(|txn| v0::migrate::Schema {
192///     author: txn.migrate_ok(|old: Lazy<v0::User>| v0::migrate::Author {
193///         name: old.name.clone(),
194///     }),
195/// });
196/// #     m.finish().expect("database version is after supported versions")
197/// # }
198/// ```
199///
200/// # `#[no_reference]` Attribute
201/// You can put this attribute on your table definitions and it will make it impossible
202/// to have foreign key references to such table.
203/// This makes it possible to use `TransactionWeak::delete_ok`.
204#[proc_macro_attribute]
205pub fn schema(
206    attr: proc_macro::TokenStream,
207    item: proc_macro::TokenStream,
208) -> proc_macro::TokenStream {
209    let name = syn::parse_macro_input!(attr as syn::Ident);
210    let item = syn::parse_macro_input!(item as ItemMod);
211
212    match generate(name, item) {
213        Ok(x) => x,
214        Err(e) => e.into_compile_error(),
215    }
216    .into()
217}
218
219/// Derive [Select] to create a new `*Select` struct.
220///
221/// This `*Select` struct will implement the `IntoSelect` trait and can be used with `Query::into_vec`
222/// or `Transaction::query_one`.
223///
224/// Usage can also be nested.
225///
226/// Example:
227/// ```
228/// #[rust_query::migration::schema(Schema)]
229/// pub mod vN {
230///     pub struct Thing {
231///         pub details: Details,
232///         pub beta: f64,
233///         pub seconds: i64,
234///     }
235///     pub struct Details {
236///         pub name: String,
237///     }
238/// }
239/// use v0::*;
240/// use rust_query::{Table, Select, Transaction};
241///
242/// #[derive(Select)]
243/// struct MyData {
244///     seconds: i64,
245///     is_it_real: bool,
246///     name: String,
247///     other: OtherData
248/// }
249///
250/// #[derive(Select)]
251/// struct OtherData {
252///     alpha: f64,
253///     beta: f64,
254/// }
255///
256/// fn do_query(db: &Transaction<Schema>) -> Vec<MyData> {
257///     db.query(|rows| {
258///         let thing = rows.join(Thing);
259///
260///         rows.into_vec(MyDataSelect {
261///             seconds: &thing.seconds,
262///             is_it_real: thing.seconds.lt(100),
263///             name: &thing.details.name,
264///             other: OtherDataSelect {
265///                 alpha: thing.beta.add(2.0),
266///                 beta: &thing.beta,
267///             },
268///         })
269///     })
270/// }
271/// # fn main() {}
272/// ```
273#[proc_macro_derive(Select)]
274pub fn from_row(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
275    let item = syn::parse_macro_input!(item as ItemStruct);
276    match dummy_impl(item) {
277        Ok(x) => x,
278        Err(e) => e.into_compile_error(),
279    }
280    .into()
281}
282
283/// Use in combination with `#[rust_query(From = Thing)]` to specify which tables
284/// this struct should implement `FromExpr` for.
285///
286/// The implementation of `FromExpr` will initialize every field from the column with
287/// the corresponding name. It is also possible to change the type of each field
288/// as long as the new field type implements `FromExpr`.
289///
290/// ```
291/// # use rust_query::migration::schema;
292/// # use rust_query::{TableRow, FromExpr};
293/// #[schema(Example)]
294/// pub mod vN {
295///     pub struct User {
296///         pub name: String,
297///         pub score: i64,
298///         pub best_game: Option<Game>,
299///     }
300///     pub struct Game;
301/// }
302///
303/// #[derive(FromExpr)]
304/// #[rust_query(From = v0::User)]
305/// struct MyUserFields {
306///     name: String,
307///     best_game: Option<TableRow<v0::Game>>,
308/// }
309/// # fn main() {}
310/// ```
311#[proc_macro_derive(FromExpr, attributes(rust_query))]
312pub fn from_expr_macro(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
313    let item = syn::parse_macro_input!(item as ItemStruct);
314    match from_expr(item) {
315        Ok(x) => x,
316        Err(e) => e.into_compile_error(),
317    }
318    .into()
319}
320
321fn make_generic(name: &Ident) -> Ident {
322    let normalized = name.to_string().to_upper_camel_case();
323    format_ident!("_{normalized}")
324}
325
326fn to_lower(name: &Ident) -> Ident {
327    let normalized = name.to_string().to_snek_case();
328    format_ident!("{normalized}")
329}
330
331fn generate(schema_name: Ident, item: syn::ItemMod) -> syn::Result<TokenStream> {
332    let schema = VersionedSchema::parse(item)?;
333
334    let mut output = quote! {};
335    let mut prev_mod = None;
336
337    let mut iter = schema
338        .versions
339        .clone()
340        .map(|version| Ok((version, schema.get(version)?)))
341        .collect::<syn::Result<Vec<_>>>()?
342        .into_iter()
343        .peekable();
344
345    while let Some((version, mut new_tables)) = iter.next() {
346        let next_mod = iter
347            .peek()
348            .map(|(peek_version, _)| format_ident!("v{peek_version}"));
349        let mut mod_output =
350            define_all_tables(&schema_name, &prev_mod, &next_mod, version, &mut new_tables)?;
351
352        let new_mod = format_ident!("v{version}");
353
354        if let Some((peek_version, peek_tables)) = iter.peek() {
355            let peek_mod = format_ident!("v{peek_version}");
356            let m = migrations::migrations(
357                &schema_name,
358                new_tables,
359                peek_tables,
360                quote! {super},
361                quote! {super::super::#peek_mod},
362            )?;
363            mod_output.extend(quote! {
364                pub mod migrate {
365                    #m
366                }
367            });
368        }
369
370        output.extend(quote! {
371            pub mod #new_mod {
372                #mod_output
373            }
374        });
375
376        prev_mod = Some(new_mod);
377    }
378
379    Ok(output)
380}