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