1use std::fmt::Write;
4
5use mssql_types::ToSql;
6
7#[derive(Debug, Clone)]
11pub struct Query {
12 sql: String,
13 }
15
16impl Query {
17 #[must_use]
19 pub fn new(sql: impl Into<String>) -> Self {
20 Self { sql: sql.into() }
21 }
22
23 #[must_use]
25 pub fn sql(&self) -> &str {
26 &self.sql
27 }
28}
29
30pub trait QueryExt {
32 fn bind<T: ToSql>(self, value: &T) -> BoundQuery<'_>;
34}
35
36pub struct BoundQuery<'a> {
38 sql: &'a str,
39 params: Vec<&'a dyn ToSql>,
40}
41
42impl<'a> BoundQuery<'a> {
43 pub fn new(sql: &'a str) -> Self {
45 Self {
46 sql,
47 params: Vec::new(),
48 }
49 }
50
51 pub fn bind<T: ToSql>(mut self, value: &'a T) -> Self {
53 self.params.push(value);
54 self
55 }
56
57 #[must_use]
59 pub fn sql(&self) -> &str {
60 self.sql
61 }
62
63 #[must_use]
65 pub fn params(&self) -> &[&dyn ToSql] {
66 &self.params
67 }
68}
69
70pub 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!(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}