sqlx_named_bind/
query.rs

1use crate::builder::build_query;
2use regex::Regex;
3use sqlx::mysql::MySqlArguments;
4use sqlx::query::Query;
5use sqlx::{mysql::MySqlQueryResult, Executor, MySql};
6
7/// Type alias for SQLx Query with MySQL arguments
8pub type Q<'q> = Query<'q, MySql, MySqlArguments>;
9
10/// A prepared query builder that supports named placeholders.
11///
12/// `PreparedQuery` allows you to use named placeholders (`:name`) in your SQL templates
13/// instead of positional placeholders (`?`). It avoids self-referential lifetime issues
14/// by storing the SQL template, placeholder order, and binder function separately,
15/// and constructing the actual `Query` on each execution.
16///
17/// # Type Parameters
18///
19/// * `F` - A binder function that binds values to placeholders. Must work with any lifetime `'q`.
20///
21/// # Examples
22///
23/// ```rust,no_run
24/// use sqlx::MySqlPool;
25/// use sqlx_named_bind::PreparedQuery;
26///
27/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
28/// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
29/// let user_id = 42;
30/// let name = "John Doe";
31///
32/// let mut query = PreparedQuery::new(
33///     "INSERT INTO users (user_id, name) VALUES (:user_id, :name)",
34///     |q, key| match key {
35///         ":user_id" => q.bind(user_id),
36///         ":name" => q.bind(name),
37///         _ => q,
38///     }
39/// )?;
40///
41/// let result = query.execute(&pool).await?;
42/// println!("Inserted {} rows", result.rows_affected());
43/// # Ok(())
44/// # }
45/// ```
46///
47/// # Using with Transactions
48///
49/// ```rust,no_run
50/// use sqlx::{MySqlPool, Transaction, MySql};
51/// use sqlx_named_bind::PreparedQuery;
52///
53/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
54/// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
55/// let mut tx: Transaction<MySql> = pool.begin().await?;
56///
57/// let mut query = PreparedQuery::new(
58///     "UPDATE users SET name = :name WHERE user_id = :user_id",
59///     |q, key| match key {
60///         ":user_id" => q.bind(vec![1, 2, 3]),
61///         ":name" => q.bind("Jane Doe"),
62///         _ => q,
63///     }
64/// )?;
65///
66/// query.execute(&mut *tx).await?;
67/// tx.commit().await?;
68/// # Ok(())
69/// # }
70/// ```
71pub struct PreparedQuery<F> {
72    sql: String,
73    order: Vec<String>,
74    binder: F,
75}
76
77impl<F> PreparedQuery<F>
78where
79    F: for<'q> FnMut(Q<'q>, &str) -> Q<'q>,
80{
81    /// Creates a new `PreparedQuery` from an SQL template and binder function.
82    ///
83    /// The SQL template can contain named placeholders in the format `:name`.
84    /// The binder function will be called for each placeholder in the order they appear.
85    ///
86    /// # Arguments
87    ///
88    /// * `template` - SQL query template with named placeholders (e.g., `:user_id`)
89    /// * `binder` - Function that binds values to placeholders based on their names
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if the SQL template cannot be parsed (invalid regex pattern).
94    ///
95    /// # Examples
96    ///
97    /// ```rust
98    /// use sqlx_named_bind::PreparedQuery;
99    ///
100    /// let query = PreparedQuery::new(
101    ///     "SELECT * FROM users WHERE id = :id",
102    ///     |q, key| match key {
103    ///         ":id" => q.bind(42),
104    ///         _ => q,
105    ///     }
106    /// )?;
107    /// # Ok::<(), sqlx_named_bind::Error>(())
108    /// ```
109    pub fn new<T>(template: T, binder: F) -> crate::Result<Self>
110    where
111        T: Into<String>,
112    {
113        let template = template.into();
114        let order = Regex::new(r":[a-zA-Z0-9_]+")?
115            .find_iter(&template)
116            .map(|m| m.as_str().to_owned())
117            .collect();
118        let sql = build_query(&template)?;
119        Ok(Self { sql, order, binder })
120    }
121
122    /// Executes the prepared query using the provided executor.
123    ///
124    /// This method constructs a fresh `Query` on each call, avoiding self-referential
125    /// lifetime issues. It works with any SQLx `Executor` implementation, including
126    /// `MySqlPool`, `Transaction`, and others.
127    ///
128    /// # Arguments
129    ///
130    /// * `executor` - Any SQLx executor (pool, transaction, etc.)
131    ///
132    /// # Returns
133    ///
134    /// Returns the MySQL query result containing information about affected rows,
135    /// last insert ID, etc.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the database query fails.
140    ///
141    /// # Examples
142    ///
143    /// ```rust,no_run
144    /// use sqlx::MySqlPool;
145    /// use sqlx_named_bind::PreparedQuery;
146    ///
147    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
148    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
149    /// let mut query = PreparedQuery::new(
150    ///     "DELETE FROM users WHERE id = :id",
151    ///     |q, key| match key {
152    ///         ":id" => q.bind(42),
153    ///         _ => q,
154    ///     }
155    /// )?;
156    ///
157    /// let result = query.execute(&pool).await?;
158    /// println!("Deleted {} rows", result.rows_affected());
159    /// # Ok(())
160    /// # }
161    /// ```
162    pub async fn execute<'e, E>(&mut self, executor: E) -> crate::Result<MySqlQueryResult>
163    where
164        E: Executor<'e, Database = MySql>,
165    {
166        let &mut PreparedQuery {
167            ref sql,
168            ref order,
169            ref mut binder,
170        } = self;
171
172        let mut q = sqlx::query::<MySql>(sql);
173        for key in order.iter() {
174            q = binder(q, key);
175        }
176        Ok(q.execute(executor).await?)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_prepared_query_new() {
186        let result = PreparedQuery::new(
187            "SELECT * FROM users WHERE id = :id",
188            |q, _| q,
189        );
190        assert!(result.is_ok());
191    }
192
193    #[test]
194    fn test_prepared_query_placeholder_order() {
195        let query = PreparedQuery::new(
196            "SELECT * FROM users WHERE id = :id AND name = :name",
197            |q, _| q,
198        ).unwrap();
199
200        assert_eq!(query.order, vec![":id", ":name"]);
201        assert_eq!(query.sql, "SELECT * FROM users WHERE id = ? AND name = ?");
202    }
203
204    #[test]
205    fn test_prepared_query_repeated_placeholders() {
206        let query = PreparedQuery::new(
207            "SELECT * FROM users WHERE id = :id OR user_id = :id",
208            |q, _| q,
209        ).unwrap();
210
211        // Both occurrences should be captured
212        assert_eq!(query.order, vec![":id", ":id"]);
213        assert_eq!(query.sql, "SELECT * FROM users WHERE id = ? OR user_id = ?");
214    }
215}