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