1use super::value::{SqlValue, ToSqlValue};
6
7#[must_use]
11pub fn dyn_col(name: &str) -> ColumnRef {
12 ColumnRef {
13 table: None,
14 name: String::from(name),
15 }
16}
17
18#[derive(Debug, Clone)]
20pub struct ColumnRef {
21 pub table: Option<String>,
23 pub name: String,
25}
26
27impl ColumnRef {
28 #[must_use]
30 pub fn qualified(table: &str, name: &str) -> Self {
31 Self {
32 table: Some(String::from(table)),
33 name: String::from(name),
34 }
35 }
36
37 #[must_use]
39 pub fn to_sql(&self) -> String {
40 match &self.table {
41 Some(t) => format!("{t}.{}", self.name),
42 None => self.name.clone(),
43 }
44 }
45
46 #[must_use]
48 pub fn eq<T: ToSqlValue>(self, value: T) -> ExprBuilder {
49 ExprBuilder::binary(self.into(), "=", value.to_sql_value().into())
50 }
51
52 #[must_use]
54 pub fn not_eq<T: ToSqlValue>(self, value: T) -> ExprBuilder {
55 ExprBuilder::binary(self.into(), "!=", value.to_sql_value().into())
56 }
57
58 #[must_use]
60 pub fn lt<T: ToSqlValue>(self, value: T) -> ExprBuilder {
61 ExprBuilder::binary(self.into(), "<", value.to_sql_value().into())
62 }
63
64 #[must_use]
66 pub fn lt_eq<T: ToSqlValue>(self, value: T) -> ExprBuilder {
67 ExprBuilder::binary(self.into(), "<=", value.to_sql_value().into())
68 }
69
70 #[must_use]
72 pub fn gt<T: ToSqlValue>(self, value: T) -> ExprBuilder {
73 ExprBuilder::binary(self.into(), ">", value.to_sql_value().into())
74 }
75
76 #[must_use]
78 pub fn gt_eq<T: ToSqlValue>(self, value: T) -> ExprBuilder {
79 ExprBuilder::binary(self.into(), ">=", value.to_sql_value().into())
80 }
81
82 #[must_use]
84 pub fn is_null(self) -> ExprBuilder {
85 ExprBuilder::postfix(self.into(), "IS NULL")
86 }
87
88 #[must_use]
90 pub fn is_not_null(self) -> ExprBuilder {
91 ExprBuilder::postfix(self.into(), "IS NOT NULL")
92 }
93
94 #[must_use]
96 pub fn like<T: ToSqlValue>(self, pattern: T) -> ExprBuilder {
97 ExprBuilder::binary(self.into(), "LIKE", pattern.to_sql_value().into())
98 }
99
100 #[must_use]
102 pub fn not_like<T: ToSqlValue>(self, pattern: T) -> ExprBuilder {
103 ExprBuilder::binary(self.into(), "NOT LIKE", pattern.to_sql_value().into())
104 }
105
106 #[must_use]
108 pub fn between<T: ToSqlValue, U: ToSqlValue>(self, low: T, high: U) -> ExprBuilder {
109 ExprBuilder::between(self.into(), low.to_sql_value(), high.to_sql_value(), false)
110 }
111
112 #[must_use]
114 pub fn not_between<T: ToSqlValue, U: ToSqlValue>(self, low: T, high: U) -> ExprBuilder {
115 ExprBuilder::between(self.into(), low.to_sql_value(), high.to_sql_value(), true)
116 }
117
118 #[must_use]
120 pub fn in_list<T: ToSqlValue>(self, values: Vec<T>) -> ExprBuilder {
121 let sql_values: Vec<SqlValue> = values.into_iter().map(ToSqlValue::to_sql_value).collect();
122 ExprBuilder::in_list_impl(self.into(), sql_values, false)
123 }
124
125 #[must_use]
127 pub fn not_in_list<T: ToSqlValue>(self, values: Vec<T>) -> ExprBuilder {
128 let sql_values: Vec<SqlValue> = values.into_iter().map(ToSqlValue::to_sql_value).collect();
129 ExprBuilder::in_list_impl(self.into(), sql_values, true)
130 }
131}
132
133#[derive(Debug, Clone)]
135pub struct ExprBuilder {
136 sql: String,
137 params: Vec<SqlValue>,
138}
139
140impl ExprBuilder {
141 #[must_use]
145 pub fn raw(sql: impl Into<String>) -> Self {
146 Self {
147 sql: sql.into(),
148 params: vec![],
149 }
150 }
151
152 #[must_use]
156 pub fn column(name: &str) -> Self {
157 Self {
158 sql: String::from(name),
159 params: vec![],
160 }
161 }
162
163 #[must_use]
165 pub fn value<T: ToSqlValue>(value: T) -> Self {
166 Self {
167 sql: String::from("?"),
168 params: vec![value.to_sql_value()],
169 }
170 }
171
172 fn binary(left: Self, op: &str, right: Self) -> Self {
174 let mut params = left.params;
175 params.extend(right.params);
176 Self {
177 sql: format!("{} {op} {}", left.sql, right.sql),
178 params,
179 }
180 }
181
182 fn postfix(operand: Self, op: &str) -> Self {
184 Self {
185 sql: format!("{} {op}", operand.sql),
186 params: operand.params,
187 }
188 }
189
190 fn between(expr: Self, low: SqlValue, high: SqlValue, negated: bool) -> Self {
192 let keyword = if negated { "NOT BETWEEN" } else { "BETWEEN" };
193 let mut params = expr.params;
194 params.push(low);
195 params.push(high);
196 Self {
197 sql: format!("{} {keyword} ? AND ?", expr.sql),
198 params,
199 }
200 }
201
202 fn in_list_impl(expr: Self, values: Vec<SqlValue>, negated: bool) -> Self {
204 let keyword = if negated { "NOT IN" } else { "IN" };
205 let placeholders: Vec<&str> = values.iter().map(|_| "?").collect();
206 let mut params = expr.params;
207 params.extend(values);
208 Self {
209 sql: format!("{} {keyword} ({})", expr.sql, placeholders.join(", ")),
210 params,
211 }
212 }
213
214 #[must_use]
216 pub fn and(self, other: Self) -> Self {
217 Self::binary(self, "AND", other)
218 }
219
220 #[must_use]
222 pub fn or(self, other: Self) -> Self {
223 Self::binary(self, "OR", other)
224 }
225
226 #[must_use]
228 pub fn paren(self) -> Self {
229 Self {
230 sql: format!("({})", self.sql),
231 params: self.params,
232 }
233 }
234
235 #[must_use]
237 #[allow(clippy::should_implement_trait)]
238 pub fn not(self) -> Self {
239 Self {
240 sql: format!("NOT {}", self.sql),
241 params: self.params,
242 }
243 }
244
245 #[must_use]
247 pub fn eq<T: ToSqlValue>(self, value: T) -> Self {
248 Self::binary(self, "=", value.to_sql_value().into())
249 }
250
251 #[must_use]
253 pub fn not_eq<T: ToSqlValue>(self, value: T) -> Self {
254 Self::binary(self, "!=", value.to_sql_value().into())
255 }
256
257 #[must_use]
259 pub fn lt<T: ToSqlValue>(self, value: T) -> Self {
260 Self::binary(self, "<", value.to_sql_value().into())
261 }
262
263 #[must_use]
265 pub fn lt_eq<T: ToSqlValue>(self, value: T) -> Self {
266 Self::binary(self, "<=", value.to_sql_value().into())
267 }
268
269 #[must_use]
271 pub fn gt<T: ToSqlValue>(self, value: T) -> Self {
272 Self::binary(self, ">", value.to_sql_value().into())
273 }
274
275 #[must_use]
277 pub fn gt_eq<T: ToSqlValue>(self, value: T) -> Self {
278 Self::binary(self, ">=", value.to_sql_value().into())
279 }
280
281 #[must_use]
283 pub fn is_null(self) -> Self {
284 Self::postfix(self, "IS NULL")
285 }
286
287 #[must_use]
289 pub fn is_not_null(self) -> Self {
290 Self::postfix(self, "IS NOT NULL")
291 }
292
293 #[must_use]
295 pub fn like<T: ToSqlValue>(self, pattern: T) -> Self {
296 Self::binary(self, "LIKE", pattern.to_sql_value().into())
297 }
298
299 #[must_use]
301 pub fn in_list<T: ToSqlValue>(self, values: Vec<T>) -> Self {
302 let sql_values: Vec<SqlValue> = values.into_iter().map(ToSqlValue::to_sql_value).collect();
303 Self::in_list_impl(self, sql_values, false)
304 }
305
306 #[must_use]
308 pub fn not_in_list<T: ToSqlValue>(self, values: Vec<T>) -> Self {
309 let sql_values: Vec<SqlValue> = values.into_iter().map(ToSqlValue::to_sql_value).collect();
310 Self::in_list_impl(self, sql_values, true)
311 }
312
313 #[must_use]
315 pub fn sql(&self) -> &str {
316 &self.sql
317 }
318
319 #[must_use]
321 pub fn params(&self) -> &[SqlValue] {
322 &self.params
323 }
324
325 #[must_use]
327 pub fn build(self) -> (String, Vec<SqlValue>) {
328 (self.sql, self.params)
329 }
330}
331
332impl From<ColumnRef> for ExprBuilder {
333 fn from(col: ColumnRef) -> Self {
334 Self {
335 sql: col.to_sql(),
336 params: vec![],
337 }
338 }
339}
340
341impl From<SqlValue> for ExprBuilder {
342 fn from(value: SqlValue) -> Self {
343 Self {
344 sql: String::from("?"),
345 params: vec![value],
346 }
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn test_column_eq() {
356 let expr = dyn_col("name").eq("Alice");
357 assert_eq!(expr.sql(), "name = ?");
358 assert_eq!(expr.params().len(), 1);
359 }
360
361 #[test]
362 fn test_column_comparison() {
363 assert_eq!(dyn_col("age").gt(18).sql(), "age > ?");
364 assert_eq!(dyn_col("age").lt_eq(65).sql(), "age <= ?");
365 }
366
367 #[test]
368 fn test_is_null() {
369 let expr = dyn_col("deleted_at").is_null();
370 assert_eq!(expr.sql(), "deleted_at IS NULL");
371 assert!(expr.params().is_empty());
372 }
373
374 #[test]
375 fn test_like() {
376 let expr = dyn_col("email").like("%@example.com");
377 assert_eq!(expr.sql(), "email LIKE ?");
378 }
379
380 #[test]
381 fn test_between() {
382 let expr = dyn_col("price").between(10, 100);
383 assert_eq!(expr.sql(), "price BETWEEN ? AND ?");
384 assert_eq!(expr.params().len(), 2);
385 }
386
387 #[test]
388 fn test_in_list() {
389 let expr = dyn_col("status").in_list(vec!["active", "pending"]);
390 assert_eq!(expr.sql(), "status IN (?, ?)");
391 assert_eq!(expr.params().len(), 2);
392 }
393
394 #[test]
395 fn test_and_or() {
396 let expr = dyn_col("active").eq(true).and(
397 dyn_col("age")
398 .gt(18)
399 .or(dyn_col("verified").eq(true))
400 .paren(),
401 );
402 assert_eq!(expr.sql(), "active = ? AND (age > ? OR verified = ?)");
403 assert_eq!(expr.params().len(), 3);
404 }
405
406 #[test]
407 fn test_qualified_column() {
408 let expr = ColumnRef::qualified("users", "name").eq("Bob");
409 assert_eq!(expr.sql(), "users.name = ?");
410 }
411
412 #[test]
413 fn test_sql_injection_prevention() {
414 let malicious = "'; DROP TABLE users; --";
415 let expr = dyn_col("name").eq(malicious);
416 assert_eq!(expr.sql(), "name = ?");
418 assert!(matches!(&expr.params()[0], SqlValue::Text(s) if s == malicious));
420 }
421}