16 releases
| 0.3.1 | Jun 22, 2026 |
|---|---|
| 0.3.0 | Jun 22, 2026 |
| 0.2.8 | May 2, 2026 |
| 0.2.7 | Mar 18, 2022 |
| 0.1.4 | Jan 29, 2022 |
#456 in Database interfaces
31KB
266 lines
The fnsql crate provides simple type-safe wrappers around SQL queries.
Instead of calling type-less .query() and .execute(), you call auto-generated
unique wrappers that are strongly typed: .query_<name>() and .execute_<name>().
You manually specify the input and output types, but only once, with the query,
in separation from the code that uses the query.
It's a very simple implementation that doesn't force any schema or ORM down
your throat, so if you are already using the sqlx or postgres crates,
you can gradually replace your type-less queries with the type-ful wrappers,
or migrate from an opinionated ORM.
Quick start (sqlx_sqlite)
fnsql::fnsql! {
#[sqlx_sqlite, test]
create_table_pet() {
"CREATE TABLE pet (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
data BLOB
)"
}
#[sqlx_sqlite, test(with=[create_table_pet])]
insert_new_pet(name: String, data: Option<Vec<u8>>) {
"INSERT INTO pet (name, data) VALUES (:name, :data)"
}
#[sqlx_sqlite, test(with=[create_table_pet])]
get_pet_id_data(name: Option<String>) -> [(i32, Option<Vec<u8>>)] {
"SELECT id, data FROM pet WHERE pet.name = :name"
}
}
The generated methods are extension methods on sqlx::SqlitePool:
# async fn example() -> Result<(), sqlx::Error> {
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await?;
// DDL/DML — returns rows affected
pool.execute_create_table_pet().await?;
pool.execute_insert_new_pet(&"Max".to_string(), &None).await?;
// Query returning multiple rows as Vec of typed tuples
let rows = pool.query_get_pet_id_data(&Some("Max".to_string())).await?;
for (id, data) in rows {
println!("Found pet id={:?}, data={:?}", id, data);
}
// Query returning exactly one row
let (id, data) = pool.query_one_get_pet_id_data(&Some("Max".to_string())).await?;
// Query that may return zero or one row
let row = pool.query_opt_get_pet_id_data(&Some("Nonexistent".to_string())).await?;
# Ok(()) }
Quick start (postgres)
For postgres, named parameters are transformed to positional $1, $2, etc.
when the named attribute is used:
fnsql::fnsql! {
#[postgres]
create_table_pet() {
"CREATE TABLE pet (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
}
#[postgres, named]
insert_new_pet(id: i32, name: String) {
"INSERT INTO pet (id, name) VALUES (:id, :name)"
}
}
Generated methods are extension methods on postgres::Client and
postgres::Transaction<'a>, with both direct and prepared variants:
# fn example(conn: &mut postgres::Client) -> Result<(), postgres::Error> {
conn.execute_create_table_pet()?;
conn.execute_insert_new_pet(&1, &"Max".to_string())?;
// Prepared statement variant
let prep = conn.prepare_insert_new_pet()?;
conn.execute_prepared_insert_new_pet(&prep, &2, &"Rex".to_string())?;
# Ok(()) }
Generated API
sqlx_sqlite (sqlx::SqlitePool)
For each query <name>(p1: T1, p2: T2) -> [(O1, O2)], the following async
methods are generated on sqlx::SqlitePool:
| Method | Return Type | Description |
|---|---|---|
execute_<name>(&self, &p1, &p2) |
Result<u64, sqlx::Error> |
Execute, returns rows affected |
query_<name>(&self, &p1, &p2) |
Result<Vec<(O1, O2)>, sqlx::Error> |
Fetch all matching rows |
query_one_<name>(&self, &p1, &p2) |
Result<(O1, O2), sqlx::Error> |
Fetch exactly one row |
query_opt_<name>(&self, &p1, &p2) |
Result<Option<(O1, O2)>, sqlx::Error> |
Fetch zero or one row |
Named parameters in the SQL (:name) are automatically transformed to
positional $N placeholders at compile time.
A convert_row_<name>(row: SqliteRow) -> Result<(O1, O2), sqlx::Error>
function is also generated for manual row conversion.
postgres (postgres::Client / postgres::Transaction<'a>)
For each query <name>(p1: T1, p2: T2) -> [(O1, O2)], the following methods
are generated on both postgres::Client and postgres::Transaction<'a>:
| Method | Return Type | Description |
|---|---|---|
execute_<name>(&mut self, &p1, &p2) |
Result<u64, postgres::Error> |
Execute directly |
prepare_<name>(&mut self) |
Result<<name>Statement_, postgres::Error> |
Prepare statement |
execute_prepared_<name>(&mut self, stmt, &p1, &p2) |
Result<u64, postgres::Error> |
Execute prepared |
query_<name>(&mut self, &p1, &p2) |
Result<Vec<(O1, O2)>, postgres::Error> |
Fetch all rows |
query_prepared_<name>(&mut self, stmt, &p1, &p2) |
Result<Vec<(O1, O2)>, postgres::Error> |
Fetch all, prepared |
query_one_<name>(&mut self, &p1, &p2) |
Result<(O1, O2), postgres::Error> |
Fetch one row |
query_one_prepared_<name>(&mut self, stmt, &p1, &p2) |
Result<(O1, O2), postgres::Error> |
Fetch one, prepared |
query_opt_<name>(&mut self, &p1, &p2) |
Result<Option<(O1, O2)>, postgres::Error> |
Fetch opt row |
query_opt_prepared_<name>(&mut self, stmt, &p1, &p2) |
Result<Option<(O1, O2)>, postgres::Error> |
Fetch opt, prepared |
With the prepare-cache feature enabled, an additional prepare_cached_<name>()
method is available that uses fnsql::postgres::Cache.
Attributes
Each query declaration starts with attributes in square brackets:
#[<backend>, <attr2>, <attr3>, ...]
Backend (required, exactly one):
sqlx_sqlite— generates async methods onsqlx::SqlitePoolpostgres— generates sync methods onpostgres::Client/Transaction
Optional attributes:
test— generates an auto-test that runs the query with arbitrary valuestest(with=[other_query])— same astest, but runs prerequisite queries firstnamed(postgres only) — transforms:nameplaceholders to$1,$2, etc.conststr=<NAME>— generates apub const NAME: &strwith the query string
Parameters and return types
Parameters use Rust-like syntax: param_name: Type. The generated methods
accept references: ¶m_name. Supported types are anything that implements
the backend's respective trait (sqlx::Encode for sqlx, postgres::types::ToSql
for postgres).
Common type shortcuts:
stris accepted as a parameter type (maps to&str)[u8]is accepted as a parameter type (maps to&[u8])
Return types are optional and specified as -> [(T1, T2, ...)]. Each type
corresponds to a column in the result set. If omitted, the query is treated as
returning no data (DDL/DML).
Auto-generated tests
With the test attribute, fnsql generates a #[test] (or #[tokio::test] for
sqlx_sqlite) that opens an in-memory database, runs prerequisite queries via
test(with=[...]), and executes the query with arbitrary values. This validates
that your query syntax is correct without writing any test code.
To enable test compilation, add to your [dev-dependencies]:
arbitrary = { version = "1", features = ["derive"] }
Dependencies
~3–21MB
~247K SLoC