Skip to main content

mssql_client/
query.rs

1//! Query builder and prepared statement support.
2
3use std::fmt::Write;
4
5use mssql_types::ToSql;
6
7/// A prepared query builder.
8///
9/// Queries can be built incrementally and reused with different parameters.
10#[derive(Debug, Clone)]
11pub struct Query {
12    sql: String,
13    // Placeholder for prepared statement handle and metadata
14}
15
16impl Query {
17    /// Create a new query from SQL text.
18    #[must_use]
19    pub fn new(sql: impl Into<String>) -> Self {
20        Self { sql: sql.into() }
21    }
22
23    /// Get the SQL text.
24    #[must_use]
25    pub fn sql(&self) -> &str {
26        &self.sql
27    }
28}
29
30/// Extension trait for building parameterized queries.
31pub trait QueryExt {
32    /// Add a parameter to the query.
33    fn bind<T: ToSql>(self, value: &T) -> BoundQuery<'_>;
34}
35
36/// A query with bound parameters.
37pub struct BoundQuery<'a> {
38    sql: &'a str,
39    params: Vec<&'a dyn ToSql>,
40}
41
42impl<'a> BoundQuery<'a> {
43    /// Create a new bound query.
44    pub fn new(sql: &'a str) -> Self {
45        Self {
46            sql,
47            params: Vec::new(),
48        }
49    }
50
51    /// Add another parameter.
52    pub fn bind<T: ToSql>(mut self, value: &'a T) -> Self {
53        self.params.push(value);
54        self
55    }
56
57    /// Get the SQL text.
58    #[must_use]
59    pub fn sql(&self) -> &str {
60        self.sql
61    }
62
63    /// Get the bound parameters.
64    #[must_use]
65    pub fn params(&self) -> &[&dyn ToSql] {
66        &self.params
67    }
68}
69
70/// Generate an IN clause SQL fragment with positional parameters.
71///
72/// Returns a string like `(@p1, @p2, @p3)` for use in `WHERE column IN (...)`
73/// queries. The `start` parameter controls where numbering begins (1-based),
74/// allowing composition with other parameters in the same query.
75///
76/// # Panics
77///
78/// Panics if `count` is 0 (SQL Server rejects empty IN clauses).
79///
80/// # Examples
81///
82/// ```
83/// use mssql_client::in_params;
84///
85/// // Simple: IN clause as the only parameterized part
86/// let ids = vec![10i32, 20, 30];
87/// let sql = format!("SELECT * FROM users WHERE id IN {}", in_params(1, ids.len()));
88/// assert_eq!(sql, "SELECT * FROM users WHERE id IN (@p1, @p2, @p3)");
89///
90/// // Composed: other params before the IN clause
91/// // WHERE status = @p1 AND id IN (@p2, @p3, @p4)
92/// let fragment = in_params(2, 3);
93/// assert_eq!(fragment, "(@p2, @p3, @p4)");
94/// ```
95pub fn in_params(start: usize, count: usize) -> String {
96    assert!(count > 0, "IN clause requires at least one parameter");
97    let mut s = String::with_capacity(count * 5);
98    s.push('(');
99    for i in 0..count {
100        if i > 0 {
101            s.push_str(", ");
102        }
103        // write! on String is infallible
104        write!(s, "@p{}", start + i).unwrap();
105    }
106    s.push(')');
107    s
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_query_new() {
116        let query = Query::new("SELECT * FROM users");
117        assert_eq!(query.sql(), "SELECT * FROM users");
118    }
119
120    #[test]
121    fn test_query_new_from_string() {
122        let sql = String::from("SELECT id FROM products");
123        let query = Query::new(sql);
124        assert_eq!(query.sql(), "SELECT id FROM products");
125    }
126
127    #[test]
128    fn test_query_clone() {
129        let query = Query::new("SELECT 1");
130        let cloned = query.clone();
131        assert_eq!(cloned.sql(), "SELECT 1");
132    }
133
134    #[test]
135    fn test_query_debug() {
136        let query = Query::new("SELECT 1");
137        let debug = format!("{query:?}");
138        assert!(debug.contains("SELECT 1"));
139    }
140
141    #[test]
142    fn test_bound_query_new() {
143        let bound = BoundQuery::new("SELECT * FROM users WHERE id = @p1");
144        assert_eq!(bound.sql(), "SELECT * FROM users WHERE id = @p1");
145        assert!(bound.params().is_empty());
146    }
147
148    #[test]
149    fn test_bound_query_bind_single() {
150        let id = 42i32;
151        let bound = BoundQuery::new("SELECT * FROM users WHERE id = @p1").bind(&id);
152        assert_eq!(bound.sql(), "SELECT * FROM users WHERE id = @p1");
153        assert_eq!(bound.params().len(), 1);
154    }
155
156    #[test]
157    fn test_bound_query_bind_multiple() {
158        let id = 42i32;
159        let name = "Alice";
160        let bound = BoundQuery::new("SELECT * FROM users WHERE id = @p1 AND name = @p2")
161            .bind(&id)
162            .bind(&name);
163        assert_eq!(bound.params().len(), 2);
164    }
165
166    #[test]
167    fn test_bound_query_chained_binds() {
168        let a = 1i32;
169        let b = 2i32;
170        let c = 3i32;
171        let bound = BoundQuery::new("INSERT INTO t VALUES (@p1, @p2, @p3)")
172            .bind(&a)
173            .bind(&b)
174            .bind(&c);
175        assert_eq!(bound.params().len(), 3);
176    }
177
178    #[test]
179    fn test_in_params_single() {
180        assert_eq!(in_params(1, 1), "(@p1)");
181    }
182
183    #[test]
184    fn test_in_params_multiple() {
185        assert_eq!(in_params(1, 3), "(@p1, @p2, @p3)");
186    }
187
188    #[test]
189    fn test_in_params_with_offset() {
190        assert_eq!(in_params(4, 2), "(@p4, @p5)");
191    }
192
193    #[test]
194    fn test_in_params_large() {
195        let result = in_params(1, 5);
196        assert_eq!(result, "(@p1, @p2, @p3, @p4, @p5)");
197    }
198
199    #[test]
200    fn test_in_params_format_into_sql() {
201        let sql = format!(
202            "SELECT * FROM users WHERE status = @p1 AND id IN {}",
203            in_params(2, 3)
204        );
205        assert_eq!(
206            sql,
207            "SELECT * FROM users WHERE status = @p1 AND id IN (@p2, @p3, @p4)"
208        );
209    }
210
211    #[test]
212    #[should_panic(expected = "IN clause requires at least one parameter")]
213    fn test_in_params_zero_count_panics() {
214        in_params(1, 0);
215    }
216}