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}