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}