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}