| Crates.io | sqlx-named-bind |
| lib.rs | sqlx-named-bind |
| version | 0.1.0 |
| created_at | 2025-11-07 13:21:18.397923+00 |
| updated_at | 2025-11-07 13:21:18.397923+00 |
| description | Named parameter binding for SQLx with HRTB pattern, avoiding self-referential issues |
| homepage | https://github.com/kano1101/sqlx-named-bind |
| repository | https://github.com/kano1101/sqlx-named-bind |
| max_upload_size | |
| id | 1921589 |
| size | 98,941 |
A SQLx extension that provides named parameter binding with HRTB (Higher-Rank Trait Bounds) pattern, avoiding self-referential lifetime issues.
:param_name instead of ? in your SQL queriesMySqlPool, Transaction, and any SQLx ExecutorPreparedQueryAs provides type-safe query results via FromRowAdd to your Cargo.toml:
[dependencies]
sqlx = { version = "0.8", features = ["mysql", "runtime-tokio"] }
sqlx-named-bind = "0.1"
use sqlx::MySqlPool;
use sqlx_named_bind::PreparedQuery;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool = MySqlPool::connect("mysql://localhost/test").await?;
let user_id = 42;
let name = "John Doe";
let mut query = PreparedQuery::new(
"INSERT INTO users (id, name) VALUES (:id, :name)",
|q, key| match key {
":id" => q.bind(user_id),
":name" => q.bind(name),
_ => q,
}
)?;
let result = query.execute(&pool).await?;
println!("Inserted {} rows", result.rows_affected());
Ok(())
}
use sqlx::{MySqlPool, FromRow};
use sqlx_named_bind::PreparedQueryAs;
#[derive(FromRow)]
struct User {
id: i32,
name: String,
email: String,
}
async fn find_users(pool: &MySqlPool, min_age: i32) -> Result<Vec<User>, Box<dyn std::error::Error>> {
let mut query = PreparedQueryAs::<User, _>::new(
"SELECT id, name, email FROM users WHERE age >= :min_age",
|q, key| match key {
":min_age" => q.bind(min_age),
_ => q,
}
)?;
Ok(query.fetch_all(pool).await?)
}
use sqlx::{MySqlPool, Transaction, MySql};
use sqlx_named_bind::PreparedQuery;
async fn transfer_money(
pool: &MySqlPool,
from_id: i32,
to_id: i32,
amount: i32,
) -> Result<(), Box<dyn std::error::Error>> {
let mut tx: Transaction<MySql> = pool.begin().await?;
let mut debit = PreparedQuery::new(
"UPDATE accounts SET balance = balance - :amount WHERE id = :id",
|q, key| match key {
":amount" => q.bind(amount),
":id" => q.bind(from_id),
_ => q,
}
)?;
let mut credit = PreparedQuery::new(
"UPDATE accounts SET balance = balance + :amount WHERE id = :id",
|q, key| match key {
":amount" => q.bind(amount),
":id" => q.bind(to_id),
_ => q,
}
)?;
debit.execute(&mut *tx).await?;
credit.execute(&mut *tx).await?;
tx.commit().await?;
Ok(())
}
use sqlx::{MySqlPool, FromRow};
use sqlx_named_bind::PreparedQueryAs;
#[derive(FromRow)]
struct User {
id: i32,
name: String,
}
async fn find_user_by_email(
pool: &MySqlPool,
email: &str,
) -> Result<Option<User>, Box<dyn std::error::Error>> {
let mut query = PreparedQueryAs::<User, _>::new(
"SELECT id, name FROM users WHERE email = :email",
|q, key| match key {
":email" => q.bind(email),
_ => q,
}
)?;
Ok(query.fetch_optional(pool).await?)
}
The library uses a three-step approach to avoid self-referential lifetime issues:
:name) and convert SQL to use positional placeholders (?)Query on each execution with the correct lifetimeThis approach leverages HRTB (Higher-Rank Trait Bounds) to ensure the binder function works with any lifetime, making the API both safe and flexible.
Without HRTB, you'd encounter self-referential lifetime issues:
// ❌ This doesn't work - self-referential lifetime
struct BadQuery<'a> {
query: Query<'a, MySql>, // 'a refers to data inside BadQuery
}
With HRTB, we defer the lifetime decision to call-site:
// ✅ This works - lifetime chosen at each call
where F: for<'q> FnMut(Query<'q, MySql>, &str) -> Query<'q, MySql>
PreparedQueryFor queries that execute but don't return rows (INSERT, UPDATE, DELETE).
Methods:
new(template, binder) - Create a new prepared queryexecute(executor) - Execute the query and return MySqlQueryResultPreparedQueryAs<R>For queries that return typed rows (SELECT).
Methods:
new(template, binder) - Create a new prepared queryfetch_all(executor) - Fetch all matching rowsfetch_one(executor) - Fetch exactly one row (error if 0 or >1)fetch_optional(executor) - Fetch at most one row (returns Option<R>)[a-zA-Z0-9_]+| Feature | sqlx-named-bind | SQLx native | Other libraries |
|---|---|---|---|
| Named parameters | ✅ :name |
❌ ? only |
✅ Varies |
| Type safety | ✅ Full | ✅ Full | ⚠️ Varies |
| Lifetime safety | ✅ HRTB | ✅ Native | ⚠️ Varies |
| Generic executor | ✅ Yes | ✅ Yes | ❌ Usually pool-only |
| Runtime overhead | ✅ Zero | ✅ Zero | ⚠️ Some have overhead |
Contributions are welcome! Please feel free to submit a Pull Request.
Licensed under either of:
at your option.
This library was inspired by the need for named parameter binding in SQLx while maintaining the same level of type safety and performance. Special thanks to the SQLx team for creating an excellent async SQL toolkit.