sqlx_named_bind/
lib.rs

1//! # sqlx-named-bind
2//!
3//! A SQLx extension that provides named parameter binding with HRTB (Higher-Rank Trait Bounds) pattern,
4//! avoiding self-referential lifetime issues.
5//!
6//! ## Features
7//!
8//! - **Named Placeholders**: Use `:param_name` instead of `?` in your SQL queries
9//! - **HRTB Pattern**: Avoids self-referential lifetime issues through proper use of Higher-Rank Trait Bounds
10//! - **Generic Executor Support**: Works with `MySqlPool`, `Transaction`, and any SQLx `Executor`
11//! - **Type-Safe Results**: `PreparedQueryAs` provides strongly-typed query results via `FromRow`
12//! - **Zero Runtime Overhead**: Placeholder conversion happens at query construction time
13//!
14//! ## Quick Start
15//!
16//! Add to your `Cargo.toml`:
17//!
18//! ```toml
19//! [dependencies]
20//! sqlx = { version = "0.8", features = ["mysql", "runtime-tokio"] }
21//! sqlx-named-bind = "0.1"
22//! ```
23//!
24//! ## Examples
25//!
26//! ### Basic Query Execution
27//!
28//! ```rust,no_run
29//! use sqlx::MySqlPool;
30//! use sqlx_named_bind::PreparedQuery;
31//!
32//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
33//! let pool = MySqlPool::connect("mysql://localhost/test").await?;
34//!
35//! let user_id = 42;
36//! let name = "John Doe";
37//!
38//! let mut query = PreparedQuery::new(
39//!     "INSERT INTO users (id, name) VALUES (:id, :name)",
40//!     |q, key| match key {
41//!         ":id" => q.bind(user_id),
42//!         ":name" => q.bind(name),
43//!         _ => q,
44//!     }
45//! )?;
46//!
47//! let result = query.execute(&pool).await?;
48//! println!("Inserted {} rows", result.rows_affected());
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! ### Typed Query Results
54//!
55//! ```rust,no_run
56//! use sqlx::{MySqlPool, FromRow};
57//! use sqlx_named_bind::PreparedQueryAs;
58//!
59//! #[derive(FromRow)]
60//! struct User {
61//!     id: i32,
62//!     name: String,
63//!     email: String,
64//! }
65//!
66//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
67//! # let pool = MySqlPool::connect("mysql://localhost/test").await?;
68//! let min_age = 18;
69//!
70//! let mut query = PreparedQueryAs::<User, _>::new(
71//!     "SELECT id, name, email FROM users WHERE age >= :min_age",
72//!     |q, key| match key {
73//!         ":min_age" => q.bind(min_age),
74//!         _ => q,
75//!     }
76//! )?;
77//!
78//! let users: Vec<User> = query.fetch_all(&pool).await?;
79//! for user in users {
80//!     println!("{}: {}", user.name, user.email);
81//! }
82//! # Ok(())
83//! # }
84//! ```
85//!
86//! ### Using with Transactions
87//!
88//! ```rust,no_run
89//! use sqlx::{MySqlPool, Transaction, MySql};
90//! use sqlx_named_bind::PreparedQuery;
91//!
92//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
93//! # let pool = MySqlPool::connect("mysql://localhost/test").await?;
94//! let mut tx: Transaction<MySql> = pool.begin().await?;
95//!
96//! let mut query1 = PreparedQuery::new(
97//!     "UPDATE accounts SET balance = balance - :amount WHERE id = :from_id",
98//!     |q, key| match key {
99//!         ":amount" => q.bind(100),
100//!         ":from_id" => q.bind(1),
101//!         _ => q,
102//!     }
103//! )?;
104//!
105//! let mut query2 = PreparedQuery::new(
106//!     "UPDATE accounts SET balance = balance + :amount WHERE id = :to_id",
107//!     |q, key| match key {
108//!         ":amount" => q.bind(100),
109//!         ":to_id" => q.bind(2),
110//!         _ => q,
111//!     }
112//! )?;
113//!
114//! query1.execute(&mut *tx).await?;
115//! query2.execute(&mut *tx).await?;
116//!
117//! tx.commit().await?;
118//! # Ok(())
119//! # }
120//! ```
121//!
122//! ### Optional Results
123//!
124//! ```rust,no_run
125//! use sqlx::{MySqlPool, FromRow};
126//! use sqlx_named_bind::PreparedQueryAs;
127//!
128//! #[derive(FromRow)]
129//! struct User {
130//!     id: i32,
131//!     name: String,
132//! }
133//!
134//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
135//! # let pool = MySqlPool::connect("mysql://localhost/test").await?;
136//! let email = "user@example.com";
137//!
138//! let mut query = PreparedQueryAs::<User, _>::new(
139//!     "SELECT id, name FROM users WHERE email = :email",
140//!     |q, key| match key {
141//!         ":email" => q.bind(email),
142//!         _ => q,
143//!     }
144//! )?;
145//!
146//! match query.fetch_optional(&pool).await? {
147//!     Some(user) => println!("Found user: {}", user.name),
148//!     None => println!("User not found"),
149//! }
150//! # Ok(())
151//! # }
152//! ```
153//!
154//! ## How It Works
155//!
156//! The library uses a three-step approach to avoid self-referential lifetime issues:
157//!
158//! 1. **Parse**: Extract named placeholders (`:name`) and convert SQL to use positional placeholders (`?`)
159//! 2. **Store**: Keep the converted SQL, placeholder order, and binder function separately
160//! 3. **Execute**: Construct a fresh SQLx `Query` on each execution with the correct lifetime
161//!
162//! This approach leverages HRTB (Higher-Rank Trait Bounds) to ensure the binder function
163//! works with any lifetime, making the API both safe and flexible.
164//!
165//! ## Limitations
166//!
167//! - Currently only supports MySQL (PostgreSQL and SQLite support planned)
168//! - Placeholder names must match `[a-zA-Z0-9_]+`
169//! - All placeholders in the SQL must be handled by the binder function
170//!
171//! ## License
172//!
173//! Licensed under either of Apache License, Version 2.0 or MIT license at your option.
174
175pub mod builder;
176pub mod error;
177pub mod query;
178pub mod query_as;
179
180pub use error::{Error, Result};
181pub use query::PreparedQuery;
182pub use query_as::PreparedQueryAs;
183
184/// Convenience re-exports for common use cases
185pub mod prelude {
186    pub use crate::error::{Error, Result};
187    pub use crate::PreparedQuery;
188    pub use crate::PreparedQueryAs;
189}