include-sql is a macro for using SQL in Rust.
include-sql was inspired by Yesql. It allows the programmer to write SQL queries in SQL, keep them separate from the Rust code, and easily embed them into Rust programs via the proc-macro that this library provides.
All by itself include-sql actually does very little - it reads and parses SQL file and transforms it into a call to the impl_sql
macro. It is expected that impl_sql
is provided either by the project that uses include-sql or by an external library. For example, there are several include-sql companion crates, like include-postgres-sql and include-sqlite-sql, that implement impl_sql
. They can simply be used directly if their approaches to embedding SQL are deemed appropriate and convenient. Alternatively, they can be used as a starting point when implementing your own impl_sql
.
As include-sql is not intended to be used directly, to illustrate the workflow we'll use include-sqlite-sql.
Add include-sqlite-sql
as a dependency:
toml
[dependencies]
include-sqlite-sql = "0.1"
Write your SQL and save it in a file. For example, let's say the following is saved as library.sql
in the project's src
folder:
```sql -- name: getloanedbooks? -- Returns the list of books loaned to a patron -- # Parameters -- param: userid: &str - user ID SELECT booktitle FROM library WHERE loanedto = :userid ORDER BY 1;
-- name: loanbooks! -- Updates the book records to reflect loan to a patron -- # Parameters -- param: userid: &str - user ID -- param: bookids: u32 - book IDs UPDATE library SET loanedto = :userid , loanedon = currenttimestamp WHERE bookid IN (:book_ids); ```
And then use it in Rust as:
```rust , ignore use includesqlitesql::{includesql, implsql}; use rusqlite::{Result, Connection};
include_sql!("src/library.sql");
fn main() -> Result<()> {
let args : Vec
let db = Connection::open(dbpath)?;
db.get_loaned_books(user_id, |row| {
let book_title : &str = row.get_ref("book_title")?.as_str()?;
println!("{}", book_title);
Ok(())
})?;
Ok(())
} ```
Note that the path to the SQL file must be specified relative to the project root, i.e. relative to
CARGO_MANIFEST_DIR
, even if you keep your SQL file alongside rust module that includes it. Because include-sql targets stable Rust this requirement will persist until SourceFile stabilizes.
After parsing and validating the content of the SQL file include-sql
generates the following call:
rust , ignore
impl_sql!{ LibrarySql =
{
? get_loaned_books (:user_id (&str))
" Returns the list of books loaned to a patron\n # Parameters\n * `user_id` - user ID"
$ "SELECT book_title\n FROM library\n WHERE loaned_to = " :user_id "\n ORDER BY 1"
},
{
! loan_books (:user_id (&str) #book_ids (u32))
" Updates the book records to reflect loan to a patron\n # Parameters\n * `user_id` - user ID\n * `book_ids` - book IDs"
$ "UPDATE library\n SET loaned_to = " :user_id "\n, loaned_on = current_timestamp\n WHERE book_id IN (" #book_ids ")"
}
}
Which include_sqlite_sql::impl_sql
transforms into the following implementation:
``rust , ignore
trait LibrarySql {
/// Returns the list of books loaned to a patron
/// # Parameters
/// *
userid` - user ID
fn getloanedbooks
/// Updates the book records to reflect loan to a patron
/// # Parameters
/// * `user_id` - user ID
/// * `book_ids` - book IDs
fn loan_books(&self, user_id: &str, book_ids: &[u32]) -> rusqlite::Result<usize>;
} ```
And, of course, it also implements the trait:
rust , ignore
impl LibrarySql for rusqlite::Connection {
/// ...
}
The included documentation describes the supported SQL file format and provides instructions on writing your own impl_sql
macro.