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