sqlx_named_bind/
query_as.rs

1use crate::builder::build_query;
2use regex::Regex;
3use sqlx::{
4    mysql::{MySqlArguments, MySqlRow},
5    query::QueryAs,
6    Executor, MySql,
7};
8
9/// Type alias for SQLx QueryAs with MySQL arguments
10pub type QA<'q, R> = QueryAs<'q, MySql, R, MySqlArguments>;
11
12/// A prepared query builder that returns typed results from named placeholders.
13///
14/// `PreparedQueryAs` is similar to `PreparedQuery` but returns strongly-typed results
15/// using SQLx's `FromRow` trait. It supports `fetch_all`, `fetch_one`, and `fetch_optional`.
16///
17/// # Type Parameters
18///
19/// * `R` - The result type that implements `FromRow`
20/// * `F` - A binder function that binds values to placeholders
21///
22/// # Examples
23///
24/// ```rust,no_run
25/// use sqlx::{MySqlPool, FromRow};
26/// use sqlx_named_bind::PreparedQueryAs;
27///
28/// #[derive(FromRow)]
29/// struct User {
30///     id: i32,
31///     name: String,
32/// }
33///
34/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
35/// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
36/// let user_id = 42;
37///
38/// let mut query = PreparedQueryAs::<User, _>::new(
39///     "SELECT id, name FROM users WHERE id = :id",
40///     |q, key| match key {
41///         ":id" => q.bind(user_id),
42///         _ => q,
43///     }
44/// )?;
45///
46/// let user: User = query.fetch_one(&pool).await?;
47/// println!("User: {} ({})", user.name, user.id);
48/// # Ok(())
49/// # }
50/// ```
51pub struct PreparedQueryAs<R, F>
52where
53    F: for<'q> FnMut(QA<'q, R>, &str) -> QA<'q, R>,
54{
55    sql: String,
56    order: Vec<String>,
57    binder: F,
58    _pd: std::marker::PhantomData<R>,
59}
60
61impl<R, F> PreparedQueryAs<R, F>
62where
63    for<'row> R: sqlx::FromRow<'row, MySqlRow> + Send + Unpin,
64    F: for<'q> FnMut(QA<'q, R>, &str) -> QA<'q, R>,
65{
66    /// Creates a new `PreparedQueryAs` from an SQL template and binder function.
67    ///
68    /// # Arguments
69    ///
70    /// * `template` - SQL query template with named placeholders
71    /// * `binder` - Function that binds values to placeholders
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if the SQL template cannot be parsed.
76    ///
77    /// # Examples
78    ///
79    /// ```rust
80    /// use sqlx::FromRow;
81    /// use sqlx_named_bind::PreparedQueryAs;
82    ///
83    /// #[derive(FromRow)]
84    /// struct User {
85    ///     id: i32,
86    ///     name: String,
87    /// }
88    ///
89    /// let query = PreparedQueryAs::<User, _>::new(
90    ///     "SELECT id, name FROM users WHERE id = :id",
91    ///     |q, key| match key {
92    ///         ":id" => q.bind(42),
93    ///         _ => q,
94    ///     }
95    /// )?;
96    /// # Ok::<(), sqlx_named_bind::Error>(())
97    /// ```
98    pub fn new<T>(template: T, binder: F) -> crate::Result<Self>
99    where
100        T: Into<String>,
101    {
102        let template = template.into();
103        let order = Regex::new(r":[a-zA-Z0-9_]+")?
104            .find_iter(&template)
105            .map(|m| m.as_str().to_owned())
106            .collect();
107        let sql = build_query(&template)?;
108        Ok(Self {
109            sql,
110            order,
111            binder,
112            _pd: std::marker::PhantomData,
113        })
114    }
115
116    /// Executes the query and returns all matching rows.
117    ///
118    /// # Arguments
119    ///
120    /// * `executor` - Any SQLx executor (pool, transaction, etc.)
121    ///
122    /// # Returns
123    ///
124    /// Returns a vector of all rows matching the query.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if the query fails or if any row cannot be converted to type `R`.
129    ///
130    /// # Examples
131    ///
132    /// ```rust,no_run
133    /// use sqlx::{MySqlPool, FromRow};
134    /// use sqlx_named_bind::PreparedQueryAs;
135    ///
136    /// #[derive(FromRow)]
137    /// struct User {
138    ///     id: i32,
139    ///     name: String,
140    /// }
141    ///
142    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
143    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
144    /// let mut query = PreparedQueryAs::<User, _>::new(
145    ///     "SELECT id, name FROM users WHERE age > :min_age",
146    ///     |q, key| match key {
147    ///         ":min_age" => q.bind(18),
148    ///         _ => q,
149    ///     }
150    /// )?;
151    ///
152    /// let users: Vec<User> = query.fetch_all(&pool).await?;
153    /// println!("Found {} users", users.len());
154    /// # Ok(())
155    /// # }
156    /// ```
157    pub async fn fetch_all<'e, E>(&mut self, executor: E) -> crate::Result<Vec<R>>
158    where
159        E: Executor<'e, Database = MySql>,
160    {
161        let &mut PreparedQueryAs {
162            ref sql,
163            ref order,
164            ref mut binder,
165            _pd,
166        } = self;
167
168        let mut q = sqlx::query_as(sql);
169        for key in order.iter() {
170            q = binder(q, key);
171        }
172        Ok(q.fetch_all(executor).await?)
173    }
174
175    /// Executes the query and returns exactly one row.
176    ///
177    /// # Arguments
178    ///
179    /// * `executor` - Any SQLx executor (pool, transaction, etc.)
180    ///
181    /// # Returns
182    ///
183    /// Returns the single row matching the query.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if:
188    /// - No rows are found
189    /// - More than one row is found
190    /// - The query fails
191    /// - The row cannot be converted to type `R`
192    ///
193    /// # Examples
194    ///
195    /// ```rust,no_run
196    /// use sqlx::{MySqlPool, FromRow};
197    /// use sqlx_named_bind::PreparedQueryAs;
198    ///
199    /// #[derive(FromRow)]
200    /// struct User {
201    ///     id: i32,
202    ///     name: String,
203    /// }
204    ///
205    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
206    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
207    /// let mut query = PreparedQueryAs::<User, _>::new(
208    ///     "SELECT id, name FROM users WHERE id = :id",
209    ///     |q, key| match key {
210    ///         ":id" => q.bind(42),
211    ///         _ => q,
212    ///     }
213    /// )?;
214    ///
215    /// let user: User = query.fetch_one(&pool).await?;
216    /// println!("Found user: {}", user.name);
217    /// # Ok(())
218    /// # }
219    /// ```
220    pub async fn fetch_one<'e, E>(&mut self, executor: E) -> crate::Result<R>
221    where
222        E: Executor<'e, Database = MySql>,
223    {
224        let &mut PreparedQueryAs {
225            ref sql,
226            ref order,
227            ref mut binder,
228            _pd,
229        } = self;
230
231        let mut q = sqlx::query_as(sql);
232        for key in order.iter() {
233            q = binder(q, key);
234        }
235        Ok(q.fetch_one(executor).await?)
236    }
237
238    /// Executes the query and returns at most one row.
239    ///
240    /// # Arguments
241    ///
242    /// * `executor` - Any SQLx executor (pool, transaction, etc.)
243    ///
244    /// # Returns
245    ///
246    /// Returns `Some(row)` if exactly one row matches, `None` if no rows match.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if:
251    /// - More than one row is found
252    /// - The query fails
253    /// - The row cannot be converted to type `R`
254    ///
255    /// # Examples
256    ///
257    /// ```rust,no_run
258    /// use sqlx::{MySqlPool, FromRow};
259    /// use sqlx_named_bind::PreparedQueryAs;
260    ///
261    /// #[derive(FromRow)]
262    /// struct User {
263    ///     id: i32,
264    ///     name: String,
265    /// }
266    ///
267    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
268    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
269    /// let mut query = PreparedQueryAs::<User, _>::new(
270    ///     "SELECT id, name FROM users WHERE email = :email",
271    ///     |q, key| match key {
272    ///         ":email" => q.bind("user@example.com"),
273    ///         _ => q,
274    ///     }
275    /// )?;
276    ///
277    /// match query.fetch_optional(&pool).await? {
278    ///     Some(user) => println!("Found user: {}", user.name),
279    ///     None => println!("User not found"),
280    /// }
281    /// # Ok(())
282    /// # }
283    /// ```
284    pub async fn fetch_optional<'e, E>(&mut self, executor: E) -> crate::Result<Option<R>>
285    where
286        E: Executor<'e, Database = MySql>,
287    {
288        let &mut PreparedQueryAs {
289            ref sql,
290            ref order,
291            ref mut binder,
292            _pd,
293        } = self;
294
295        let mut q = sqlx::query_as(sql);
296        for key in order.iter() {
297            q = binder(q, key);
298        }
299        Ok(q.fetch_optional(executor).await?)
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    // Mock struct for testing (requires sqlx::FromRow)
308    // In real tests, this would use a real database connection
309
310    #[test]
311    fn test_prepared_query_as_new() {
312        #[derive(sqlx::FromRow)]
313        struct TestRow {
314            #[allow(dead_code)]
315            id: i32,
316        }
317
318        let result = PreparedQueryAs::<TestRow, _>::new(
319            "SELECT id FROM users WHERE id = :id",
320            |q, _| q,
321        );
322        assert!(result.is_ok());
323    }
324
325    #[test]
326    fn test_prepared_query_as_placeholder_order() {
327        #[derive(sqlx::FromRow)]
328        struct TestRow {
329            #[allow(dead_code)]
330            id: i32,
331        }
332
333        let query = PreparedQueryAs::<TestRow, _>::new(
334            "SELECT id FROM users WHERE id = :id AND name = :name",
335            |q, _| q,
336        ).unwrap();
337
338        assert_eq!(query.order, vec![":id", ":name"]);
339        assert_eq!(query.sql, "SELECT id FROM users WHERE id = ? AND name = ?");
340    }
341}