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}