include_sql/lib.rs
1#![cfg_attr(docsrs, doc = include_str!("../README.md"))]
2
3use proc_macro;
4use syn::{ self, Token, parse::{ Parse, ParseStream } };
5use proc_macro2::{
6 TokenStream, Span, Group, Delimiter, Literal, Ident, Punct, Spacing
7};
8use quote::{ ToTokens, TokenStreamExt };
9
10mod err;
11mod sql;
12mod gen;
13mod conv;
14
15/**
16Reads and parses the specified SQL file, and generates `impl_sql` macro call.
17
18For example, if the SQL file "library.sql" has these 2 statements:
19
20```sql
21-- name: get_loaned_books?
22-- Returns the list of books loaned to a patron
23-- # Parameters
24-- param: user_id: &str - user ID
25SELECT book_title FROM library WHERE loaned_to = :user_id ORDER BY 1
26/
27
28-- name: loan_books!
29-- Updates the book records to reflect loan to a patron
30-- # Parameters
31-- param: book_ids: usize - book IDs
32-- param: user_id: &str - user ID
33UPDATE library SET loaned_to = :user_id, loaned_on = current_timestamp WHERE book_id IN ( :book_ids )
34/
35```
36
37This method would generate:
38
39```rust,no_run
40# macro_rules! impl_sql { ($($t:tt)+) => {}; }
41impl_sql!{ LibrarySql =
42 {
43 ? get_loaned_books (: user_id (&str))
44 " Returns the list of books loaned to a patron\n # Parameters\n * `user_id` - user ID"
45 $ "SELECT book_title FROM library WHERE loaned_to = " : user_id "ORDER BY 1"
46 },
47 {
48 ! loan_books (# book_ids (usize) : user_id (&str))
49 " Updates the book records to reflect loan to a patron\n # Parameters\n * `user_id` - user ID\n * `book_ids` - book IDs"
50 $ "UPDATE library SET loaned_to = " : user_id ", loaned_on = current_timestamp WHERE book_id IN ( " # book_ids " )"
51 }
52}
53```
54
55Where:
56* `LibrarySql` is a camel-cased `ident` derived from the SQL file name. It might be used by `impl_sql` to generate a trait (like [include-postgres-sql][1] and [include-sqlite-sql][2] do).
57* `?` or `!` is a statement variant selector
58* `get_loaned_books` and `loan_books` are `ident`s created from the statement names that can be used to name generated methods
59* `user_id` and `book_ids` are `ident`s that represent parameter names.
60* `:` and `#` in front of the parameter names are parameter variant tags:
61 - `:` indicates that the following parameter is a scalar
62 - `#` tags IN-list parameters.
63* The following `(&str)` and `(usize)` are Rust parameter types as declared in the SQL.
64* `$` is a helper token that could be used to generate repetitions if generated artifacts are macros.
65
66> **Note** that `param:` types are passed as parenthesized types. This is done to allow `impl_sql` match them as token trees. If a parameter type is not defined in SQL, `_` will be used in its place (this `_` drives the need to match parameter types as token trees) for which `impl_sql` is expected to generate an appropriate generic type.
67
68> **Note** also that parameter order is defined by the `param` declarations. SQL parameters that are present in the SQL code, but that are not declared as one of the `param`s, will be follow the `param` parameters in the order they are found in the SQL code.
69
70## Async
71
72When include-sql is built with the `async` feature, `impl_sql` macro will be generated with additional lifetimes for reference parameters.
73For example, the above `LibrarySql` example will look like this:
74
75```rust,no_run
76# macro_rules! impl_sql { ($($t:tt)+) => {}; }
77impl_sql!{ LibrarySql =
78 {
79 ? get_loaned_books (: user_id ('user_id &str))
80 " Returns the list of books loaned to a patron\n # Parameters\n * `user_id` - user ID"
81 $ "SELECT book_title FROM library WHERE loaned_to = " : user_id "ORDER BY 1"
82 },
83 {
84 ! loan_books (# book_ids ('book_ids usize) : user_id ('user_id &str))
85 " Updates the book records to reflect loan to a patron\n # Parameters\n * `user_id` - user ID\n * `book_ids` - book IDs"
86 $ "UPDATE library SET loaned_to = " : user_id ", loaned_on = current_timestamp WHERE book_id IN ( " # book_ids " )"
87 }
88}
89```
90
91**Note** that for IN list parameters where the list item is a reference itself additional lifetime that covers list items is also generated.
92For example, for this query:
93
94```sql
95-- name: get_users_who_loaned_books?
96-- Returns names patrons that at one time or another have loaned specified books
97-- # Parameters
98-- param: book_titles: &str - book titles
99SELECT DISTINCT first_name, last_name
100 FROM patrons
101 JOIN library ON library.loaned_to = patrons.user_id
102 WHERE book_title IN (:book_titles);
103```
104
105include-sql will generate:
106
107```rust,no_run
108# macro_rules! impl_sql { ($($t:tt)+) => {}; }
109impl_sql!{ LibrarySql =
110 {
111 ? get_users_who_loaned_books (# book_titles ('book_titles 'book_titles_item &str))
112 " Returns names patrons that at one time or another have loaned specified books\n # Parameters\n * `book_titles` - book titles"
113 $ "SELECT DISTINCT first_name, last_name FROM patrons JOIN library ON library.loaned_to = patrons.user_id WHERE book_title IN (" # book_titles ")"
114 }
115}
116```
117
118[1]: https://crates.io/crates/include-postgres-sql
119[2]: https://crates.io/crates/include-sqlite-sql
120*/
121#[proc_macro]
122pub fn include_sql(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
123 let file_path = syn::parse_macro_input!(input as syn::LitStr);
124 let path = file_path.value();
125 match read_and_parse_sql_file(&path) {
126 Ok(included_sql) => {
127 let mut tokens = TokenStream::new();
128 output_include_bytes(&path, &mut tokens);
129 if !included_sql.stmt_list.is_empty() {
130 included_sql.to_tokens(&mut tokens);
131 }
132 tokens.into()
133 }
134 Err(err) => {
135 syn::Error::new(file_path.span(), err.to_string()).to_compile_error().into()
136 }
137 }
138}
139
140/// Reads the content of the file at the `path` and parses its content.
141fn read_and_parse_sql_file(file_path: &str) -> err::Result<sql::IncludedSql> {
142 use std::path::PathBuf;
143 use std::fs;
144
145 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
146 let mut path = PathBuf::from(&manifest_dir);
147 path.push(file_path);
148 let text = fs::read_to_string(&path)?;
149 let file_name = path.file_stem().unwrap_or_default().to_str().unwrap_or_default().replace('-', "_");
150 sql::parse(&text, &file_name)
151}
152
153/// Writes a phantom call to `include_bytes` to make compiler aware of the external dependency.
154fn output_include_bytes(file_path: &str, tokens: &mut TokenStream) {
155 use std::path::PathBuf;
156
157 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
158 let mut path = PathBuf::from(&manifest_dir);
159 path.push(file_path);
160
161 tokens.append(Ident::new("const", Span::call_site()));
162 tokens.append(Ident::new("_", Span::call_site()));
163 tokens.append(Punct::new(':', Spacing::Alone));
164 tokens.append(Punct::new('&', Spacing::Alone));
165
166 let mut type_tokens = TokenStream::new();
167 type_tokens.append(Ident::new("u8", Span::call_site()));
168 tokens.append(Group::new(Delimiter::Bracket, type_tokens));
169
170 tokens.append(Punct::new('=', Spacing::Alone));
171 tokens.append(Ident::new("include_bytes", Span::call_site()));
172 tokens.append(Punct::new('!', Spacing::Alone));
173
174 let mut macro_tokens = TokenStream::new();
175 macro_tokens.append(Literal::string(path.to_str().unwrap()));
176 tokens.append(Group::new(Delimiter::Parenthesis, macro_tokens));
177
178 tokens.append(Punct::new(';', Spacing::Alone));
179}
180
181/**
182Finds the specified item (`ident`) in a list (of `idents`).
183
184Returns the item's index offset by the number after `+`.
185
186```
187let idx = include_sql::index_of!(id in [name, flag, id] + 1);
188assert_eq!(idx, 3);
189```
190*/
191#[proc_macro]
192pub fn index_of(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
193 let IndexOfArgs { param_name, start_index, stmt_params } = syn::parse_macro_input!(input as IndexOfArgs);
194 let param_lookup = stmt_params.iter().position(|param| param == ¶m_name);
195 if let Some( pos ) = param_lookup {
196 let mut tokens = TokenStream::new();
197 tokens.append(Literal::usize_unsuffixed(start_index + pos));
198 tokens.into()
199 } else {
200 syn::Error::new(param_name.span(), "no such parameter").to_compile_error().into()
201 }
202}
203
204struct IndexOfArgs {
205 param_name : syn::Ident,
206 stmt_params : syn::punctuated::Punctuated<syn::Ident, Token![,]>,
207 start_index : usize,
208}
209
210impl Parse for IndexOfArgs {
211 fn parse(input: ParseStream) -> syn::Result<Self> {
212 let param_name = input.parse()?;
213 input.parse::<Token![in]>()?;
214 let param_list;
215 syn::bracketed!(param_list in input);
216 input.parse::<Token![+]>()?;
217 let start_index : syn::LitInt = input.parse()?;
218 let start_index = start_index.base10_parse()?;
219 let stmt_params = param_list.parse_terminated(syn::Ident::parse)?;
220 Ok(Self { param_name, stmt_params, start_index })
221 }
222}