Usage
When you write your own impl_sql
, you would need to add include-sql
as a dependency:
[dependencies]
include-sql = "0.3"
It might also be prudent to re-export include-sql
macro to make the use of your crate more ergonomic:
pub use include_sql::include_sql;
The users of your implementation would then be able to generate API to access database queries as:
use your_crate::{include_sql, impl_sql};
include_sql!("src/queries.sql");
Anatomy of the Included SQL File
Let’s assume that we created the following file and saved it as library.sql
:
-- name: get_loaned_books?
--
-- Returns the list of books loaned to a patron
--
-- # Parameters
--
-- param: user_id: &str - user ID
--
SELECT book_title
FROM library
WHERE loaned_to = :user_id
ORDER BY 1
/
-- name: loan_books!
--
-- Updates the book records to reflect loan to a patron
--
-- # Parameters
--
-- param: book_titles: &str - book titles
-- param: user_id: &str - user ID
--
UPDATE library
SET loaned_to = :user_id
, loaned_on = current_timestamp
WHERE book_title IN (:book_titles)
/
An SQL file can include one or more SQL statements. Each statement must have a preceding doc-comment. The latter has a dual purpose - it provides the doc-comment text for the generated method, and it also embeds meta data about the statement that follows it:
name:
a mandatory meta comment that defines an identifier that is used to generate the database access artifact. For example, include-postgres-sql and include-sqlite-sql generate a trait method from it.
Note that include-sql will use the name as-is. If you want to avoid Rust complaining about it, use the appropriate (snake) case for it.
?
is a statement variant tag. It directsimpl_sql
to generate a specific implementation. This tag can be any sequence of Rust punctuation characters as long as they represent a single valid Rust punctuation token. This tag is optional. When it is absent, an implicit!
will be passed to theimpl_sql
macro.
For example, include-postgres-sql and include-sqlite-sql recognize
?
,!
, and->
tags. For?
they generate methods that process selected rows, for!
- methods that execute all other - non-select - statements, and for->
- methods that read data fromRETURNING
statements.
param:
is an optional description of a statement parameter. It is expressed inparameter_name : parameter_type
format. Text that followsparameter_type
is used as a doc-comment for this parameter.
include-sql uses
param:
to gather parameter types and to generate the Rust doc-comment for the parameter. For example, this line in SQL:param: user_id: &str - user ID
tells include-sql that the type ofuser_id
is&str
. It is then converted into* `user_id` - user ID
text line and included into the Rust doc-comment for the generated method.
Note that include-sql expects
parameter_type
to be a Rust type and will fail if it cannot be parsed as such.
Note that because
param:
is optional for parameters that are not explicitly described byparam:
include-sql
will create a synthetic parameter description using an inferred type_
for scalar SQL parameters or synthetic generic type[GenericType]
for IN list parameters.impl_sql
must be prepared to handle cases when a parameter type should be inferred and generate method parameter that is typed generically. See include-postgres-sql or include-sqlite-sql for an example of how it can be done.
-
The rest of the statement doc-comment lines are gathered together to form a Rust doc-comment text for the generated method.
-
:user_id
and:book_ids
are statement parameters. Each parameter starts with:
and can be anything the can be an identifier in Rust. However, as they might be used to name method parameters in Rust,include-sql
forces them into snake-case. -
The inner statement comments are allowed and will be discarded by include-sql.
Statements should be terminated with a slash - /
. However, in some cases it could be optional as the following name:
meta comment would also auto-terminate the preceding statement.
Generated impl_sql
Call
For the SQL above include-sql would generate:
impl_sql!{ LibrarySql =
{
? get_loaned_books (: user_id (&str))
"\n Returns the list of books loaned to a patron\n\n # Parameters\n\n * `user_id` - user ID\n"
$ "SELECT book_title\n FROM library\n WHERE loaned_to = " : user_id "\n ORDER BY 1"
},
{
! loan_books (# book_titles (u32) : user_id (&str))
"\n Updates the book records to reflect loan to a patron\n\n # Parameters\n\n * `book_ids` - book IDs\n * `user_id` - user ID\n"
$ "UPDATE library\n SET loaned_to = " : user_id "\n, loaned_on = current_timestamp\n WHERE book_title IN (" # book_titles ")"
}
}
Where:
-
LibrarySql
is a camel-cased identifier derived from the SQL file name. It might be used byimpl_sql
to generate a trait (like include-postgres-sql and include-sqlite-sql do). -
?
or!
is a statement variant selector -
get_loaned_books
andloan_books
areident
s created from the statement names that can be used to name generated methods -
user_id
andbook_ids
are identifiers that represent parameter names. :
and#
in front of the parameter names are parameter variant tags::
indicates that the following parameter is a scalar#
tags IN-list parameters.
- The following
(&str)
and(usize)
are Rust parameter types as declared in the SQL.
Note that 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 whichimpl_sql
is expected to generate an appropriate generic type.
$
is a helper token that could be used to generate repetitions if generated artifacts are macros.
Async
When include-sql is built with the async
feature, impl_sql
macro will be generated with additional lifetimes for reference parameters.
For example, the above LibrarySql
example will look like this:
impl_sql!{ LibrarySql =
{
? get_loaned_books (: user_id ('user_id &str))
"\n Returns the list of books loaned to a patron\n\n # Parameters\n\n * `user_id` - user ID\n"
$ "SELECT book_title FROM library WHERE loaned_to = " :user_id "ORDER BY 1"
},
{
! loan_books (: user_id ('user_id &str) # book_ids ('book_ids usize))
"\n Updates the book records to reflect loan to a patron\n\n # Parameters\n\n * `book_ids` - book IDs\n * `user_id` - user ID\n"
$ "UPDATE library SET loaned_to = " : user_id ", loaned_on = current_timestamp WHERE book_title IN ( " # book_titles " )"
}
}
Note that for IN list parameters where the list item is a reference itself additional lifetime that covers list items is also generated. For example, for this query:
-- name: get_users_who_loaned_books?
--
-- Returns names patrons to whom the specified books are loaned
--
-- # Parameters
--
-- param: book_titles: &str - book titles
SELECT DISTINCT first_name, last_name
FROM patrons
JOIN library ON library.loaned_to = patrons.user_id
WHERE book_title IN (:book_titles)
include-sql will generate:
impl_sql!{ LibrarySql =
{
? get_users_who_loaned_books (# book_titles ('book_titles 'book_titles_item &str))
" Returns names patrons to whom the specified books are loaned\n # Parameters\n * `book_titles` - book titles"
$ "SELECT DISTINCT first_name, last_name FROM patrons JOIN library ON library.loaned_to = patrons.user_id WHERE book_title IN (" # book_titles ")"
}
}
Implementation Examples
As a picture is worth a thousand words before you start implementing your own impl_sql
macro it would be advisable to review existing implementations like include-postgres-sql and include-sqlite-sql, and maybe even use one of them as a starting point.