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}