1use std::fmt::Write;
14
15use crate::column::AnyColumn;
16use crate::value::Value;
17
18#[derive(Debug, Clone)]
23pub enum Expr {
24 Eq(AnyColumn, Value),
27 Ne(AnyColumn, Value),
29 Gt(AnyColumn, Value),
31 Gte(AnyColumn, Value),
33 Lt(AnyColumn, Value),
35 Lte(AnyColumn, Value),
37
38 Between(AnyColumn, Value, Value),
41 InList(AnyColumn, Vec<Value>),
43 NotIn(AnyColumn, Vec<Value>),
45
46 Like(AnyColumn, Value),
49 ILike(AnyColumn, Value),
51
52 IsNull(AnyColumn),
55 IsNotNull(AnyColumn),
57
58 And(Box<Expr>, Box<Expr>),
61 Or(Box<Expr>, Box<Expr>),
63 Not(Box<Expr>),
65}
66
67impl Expr {
68 pub fn and(self, other: Expr) -> Expr {
70 Expr::And(Box::new(self), Box::new(other))
71 }
72
73 pub fn or(self, other: Expr) -> Expr {
75 Expr::Or(Box::new(self), Box::new(other))
76 }
77
78 pub fn not(self) -> Expr {
80 Expr::Not(Box::new(self))
81 }
82
83 pub fn to_sql(&self, sql: &mut String, params: &mut Vec<Value>, idx: &mut usize) {
90 match self {
91 Expr::Eq(col, val) => {
93 *idx += 1;
94 write!(sql, "{} = ${}", col.name(), idx).unwrap();
95 params.push(val.clone());
96 }
97 Expr::Ne(col, val) => {
98 *idx += 1;
99 write!(sql, "{} != ${}", col.name(), idx).unwrap();
100 params.push(val.clone());
101 }
102 Expr::Gt(col, val) => {
103 *idx += 1;
104 write!(sql, "{} > ${}", col.name(), idx).unwrap();
105 params.push(val.clone());
106 }
107 Expr::Gte(col, val) => {
108 *idx += 1;
109 write!(sql, "{} >= ${}", col.name(), idx).unwrap();
110 params.push(val.clone());
111 }
112 Expr::Lt(col, val) => {
113 *idx += 1;
114 write!(sql, "{} < ${}", col.name(), idx).unwrap();
115 params.push(val.clone());
116 }
117 Expr::Lte(col, val) => {
118 *idx += 1;
119 write!(sql, "{} <= ${}", col.name(), idx).unwrap();
120 params.push(val.clone());
121 }
122
123 Expr::Between(col, low, high) => {
125 *idx += 1;
126 let low_idx = *idx;
127 *idx += 1;
128 let high_idx = *idx;
129 write!(sql, "{} BETWEEN ${} AND ${}", col.name(), low_idx, high_idx).unwrap();
130 params.push(low.clone());
131 params.push(high.clone());
132 }
133 Expr::InList(col, values) => {
134 if values.is_empty() {
135 sql.push_str("1=0");
137 } else {
138 write!(sql, "{} IN (", col.name()).unwrap();
139 for (i, val) in values.iter().enumerate() {
140 if i > 0 {
141 sql.push_str(", ");
142 }
143 *idx += 1;
144 write!(sql, "${}", idx).unwrap();
145 params.push(val.clone());
146 }
147 sql.push(')');
148 }
149 }
150 Expr::NotIn(col, values) => {
151 if values.is_empty() {
152 sql.push_str("1=1");
154 } else {
155 write!(sql, "{} NOT IN (", col.name()).unwrap();
156 for (i, val) in values.iter().enumerate() {
157 if i > 0 {
158 sql.push_str(", ");
159 }
160 *idx += 1;
161 write!(sql, "${}", idx).unwrap();
162 params.push(val.clone());
163 }
164 sql.push(')');
165 }
166 }
167
168 Expr::Like(col, val) => {
170 *idx += 1;
171 write!(sql, "{} LIKE ${}", col.name(), idx).unwrap();
172 params.push(val.clone());
173 }
174 Expr::ILike(col, val) => {
175 *idx += 1;
176 write!(sql, "{} ILIKE ${}", col.name(), idx).unwrap();
177 params.push(val.clone());
178 }
179
180 Expr::IsNull(col) => {
182 write!(sql, "{} IS NULL", col.name()).unwrap();
183 }
184 Expr::IsNotNull(col) => {
185 write!(sql, "{} IS NOT NULL", col.name()).unwrap();
186 }
187
188 Expr::And(left, right) => {
190 sql.push('(');
191 left.to_sql(sql, params, idx);
192 sql.push_str(" AND ");
193 right.to_sql(sql, params, idx);
194 sql.push(')');
195 }
196 Expr::Or(left, right) => {
197 sql.push('(');
198 left.to_sql(sql, params, idx);
199 sql.push_str(" OR ");
200 right.to_sql(sql, params, idx);
201 sql.push(')');
202 }
203 Expr::Not(inner) => {
204 sql.push_str("NOT (");
205 inner.to_sql(sql, params, idx);
206 sql.push(')');
207 }
208 }
209 }
210
211 pub fn render(&self) -> (String, Vec<Value>) {
214 let mut sql = String::new();
215 let mut params = Vec::new();
216 let mut idx = 0usize;
217 self.to_sql(&mut sql, &mut params, &mut idx);
218 (sql, params)
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct OrderExpr {
225 pub(crate) column: AnyColumn,
226 pub(crate) direction: OrderDirection,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq)]
231pub enum OrderDirection {
232 Asc,
233 Desc,
234 AscNullsLast,
235 DescNullsFirst,
236}
237
238impl OrderExpr {
239 pub fn new(column: AnyColumn, direction: OrderDirection) -> Self {
240 Self { column, direction }
241 }
242
243 pub fn to_sql(&self, sql: &mut String) {
245 sql.push_str(self.column.name());
246 match self.direction {
247 OrderDirection::Asc => sql.push_str(" ASC"),
248 OrderDirection::Desc => sql.push_str(" DESC"),
249 OrderDirection::AscNullsLast => sql.push_str(" ASC NULLS LAST"),
250 OrderDirection::DescNullsFirst => sql.push_str(" DESC NULLS FIRST"),
251 }
252 }
253
254 pub fn render(&self) -> String {
256 let mut sql = String::new();
257 self.to_sql(&mut sql);
258 sql
259 }
260}
261
262#[cfg(test)]
267mod tests {
268 use super::*;
269 use crate::column::Column;
270
271 fn col_id() -> Column<i32> { Column::new("id", "users") }
272 fn col_age() -> Column<i32> { Column::new("age", "users") }
273 fn col_name() -> Column<String> { Column::new("name", "users") }
274 fn col_email() -> Column<String> { Column::new("email", "users") }
275
276 #[test]
279 fn render_eq() {
280 let (sql, params) = col_age().eq(25).render();
281 assert_eq!(sql, "age = $1");
282 assert_eq!(params, vec![Value::Int(25)]);
283 }
284
285 #[test]
286 fn render_ne() {
287 let (sql, params) = col_age().ne(0).render();
288 assert_eq!(sql, "age != $1");
289 assert_eq!(params, vec![Value::Int(0)]);
290 }
291
292 #[test]
293 fn render_gt() {
294 let (sql, params) = col_age().gt(25).render();
295 assert_eq!(sql, "age > $1");
296 assert_eq!(params, vec![Value::Int(25)]);
297 }
298
299 #[test]
300 fn render_gte() {
301 let (sql, params) = col_age().gte(18).render();
302 assert_eq!(sql, "age >= $1");
303 assert_eq!(params, vec![Value::Int(18)]);
304 }
305
306 #[test]
307 fn render_lt() {
308 let (sql, params) = col_age().lt(65).render();
309 assert_eq!(sql, "age < $1");
310 assert_eq!(params, vec![Value::Int(65)]);
311 }
312
313 #[test]
314 fn render_lte() {
315 let (sql, params) = col_age().lte(100).render();
316 assert_eq!(sql, "age <= $1");
317 assert_eq!(params, vec![Value::Int(100)]);
318 }
319
320 #[test]
323 fn render_between() {
324 let (sql, params) = col_age().between(18, 65).render();
325 assert_eq!(sql, "age BETWEEN $1 AND $2");
326 assert_eq!(params, vec![Value::Int(18), Value::Int(65)]);
327 }
328
329 #[test]
330 fn render_in_list() {
331 let (sql, params) = col_id().in_list(vec![1, 2, 3]).render();
332 assert_eq!(sql, "id IN ($1, $2, $3)");
333 assert_eq!(params, vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
334 }
335
336 #[test]
337 fn render_in_list_single() {
338 let (sql, params) = col_id().in_list(vec![42]).render();
339 assert_eq!(sql, "id IN ($1)");
340 assert_eq!(params, vec![Value::Int(42)]);
341 }
342
343 #[test]
344 fn render_in_list_empty() {
345 let (sql, params) = col_id().in_list(Vec::<i32>::new()).render();
346 assert_eq!(sql, "1=0");
347 assert!(params.is_empty());
348 }
349
350 #[test]
351 fn render_not_in() {
352 let (sql, params) = col_id().not_in(vec![4, 5]).render();
353 assert_eq!(sql, "id NOT IN ($1, $2)");
354 assert_eq!(params, vec![Value::Int(4), Value::Int(5)]);
355 }
356
357 #[test]
358 fn render_not_in_empty() {
359 let (sql, params) = col_id().not_in(Vec::<i32>::new()).render();
360 assert_eq!(sql, "1=1");
361 assert!(params.is_empty());
362 }
363
364 #[test]
367 fn render_like() {
368 let (sql, params) = col_name().like("A%").render();
369 assert_eq!(sql, "name LIKE $1");
370 assert_eq!(params, vec![Value::String("A%".into())]);
371 }
372
373 #[test]
374 fn render_ilike() {
375 let (sql, params) = col_name().ilike("alice%").render();
376 assert_eq!(sql, "name ILIKE $1");
377 assert_eq!(params, vec![Value::String("alice%".into())]);
378 }
379
380 #[test]
381 fn render_contains() {
382 let (sql, params) = col_name().contains("foo").render();
383 assert_eq!(sql, "name LIKE $1");
384 assert_eq!(params, vec![Value::String("%foo%".into())]);
385 }
386
387 #[test]
388 fn render_starts_with() {
389 let (sql, params) = col_name().starts_with("A").render();
390 assert_eq!(sql, "name LIKE $1");
391 assert_eq!(params, vec![Value::String("A%".into())]);
392 }
393
394 #[test]
395 fn render_ends_with() {
396 let (sql, params) = col_name().ends_with("z").render();
397 assert_eq!(sql, "name LIKE $1");
398 assert_eq!(params, vec![Value::String("%z".into())]);
399 }
400
401 #[test]
404 fn render_is_null() {
405 let (sql, params) = col_email().is_null().render();
406 assert_eq!(sql, "email IS NULL");
407 assert!(params.is_empty());
408 }
409
410 #[test]
411 fn render_is_not_null() {
412 let (sql, params) = col_email().is_not_null().render();
413 assert_eq!(sql, "email IS NOT NULL");
414 assert!(params.is_empty());
415 }
416
417 #[test]
420 fn render_and() {
421 let (sql, params) = col_age().gt(25).and(col_name().eq("Alice")).render();
422 assert_eq!(sql, "(age > $1 AND name = $2)");
423 assert_eq!(params, vec![Value::Int(25), Value::String("Alice".into())]);
424 }
425
426 #[test]
427 fn render_or() {
428 let (sql, params) = col_age().lt(18).or(col_age().gt(65)).render();
429 assert_eq!(sql, "(age < $1 OR age > $2)");
430 assert_eq!(params, vec![Value::Int(18), Value::Int(65)]);
431 }
432
433 #[test]
434 fn render_not() {
435 let (sql, params) = col_email().is_null().not().render();
436 assert_eq!(sql, "NOT (email IS NULL)");
437 assert!(params.is_empty());
438 }
439
440 #[test]
441 fn render_nested_and_or() {
442 let expr = col_age().gt(25).and(
444 col_name().eq("Alice").or(col_name().eq("Bob")),
445 );
446 let (sql, params) = expr.render();
447 assert_eq!(sql, "(age > $1 AND (name = $2 OR name = $3))");
448 assert_eq!(params, vec![
449 Value::Int(25),
450 Value::String("Alice".into()),
451 Value::String("Bob".into()),
452 ]);
453 }
454
455 #[test]
456 fn render_deep_nesting() {
457 let expr = col_age().lt(18).or(col_age().gt(65)).not();
459 let (sql, params) = expr.render();
460 assert_eq!(sql, "NOT ((age < $1 OR age > $2))");
461 assert_eq!(params, vec![Value::Int(18), Value::Int(65)]);
462 }
463
464 #[test]
465 fn render_complex_filter() {
466 let expr = col_age()
468 .between(18, 65)
469 .and(col_email().is_not_null())
470 .or(col_name().eq("admin"));
471 let (sql, params) = expr.render();
472 assert_eq!(
473 sql,
474 "((age BETWEEN $1 AND $2 AND email IS NOT NULL) OR name = $3)"
475 );
476 assert_eq!(params, vec![
477 Value::Int(18),
478 Value::Int(65),
479 Value::String("admin".into()),
480 ]);
481 }
482
483 #[test]
486 fn render_shared_counter() {
487 let mut sql = String::new();
489 let mut params = Vec::new();
490 let mut idx = 0usize;
491
492 col_age().gt(25).to_sql(&mut sql, &mut params, &mut idx);
494 sql.push_str(" AND ");
495 col_name().eq("Alice").to_sql(&mut sql, &mut params, &mut idx);
497
498 assert_eq!(sql, "age > $1 AND name = $2");
499 assert_eq!(idx, 2);
500 assert_eq!(params.len(), 2);
501 }
502
503 #[test]
504 fn render_many_params() {
505 let mut sql = String::new();
507 let mut params = Vec::new();
508 let mut idx = 0usize;
509
510 col_id().in_list(vec![1, 2, 3, 4, 5]).to_sql(&mut sql, &mut params, &mut idx);
511 sql.push_str(" AND ");
512 col_name().eq("test").to_sql(&mut sql, &mut params, &mut idx);
513
514 assert_eq!(sql, "id IN ($1, $2, $3, $4, $5) AND name = $6");
515 assert_eq!(params.len(), 6);
516 assert_eq!(idx, 6);
517 }
518
519 #[test]
522 fn render_order_asc() {
523 assert_eq!(col_name().asc().render(), "name ASC");
524 }
525
526 #[test]
527 fn render_order_desc() {
528 assert_eq!(col_age().desc().render(), "age DESC");
529 }
530
531 #[test]
532 fn render_order_asc_nulls_last() {
533 assert_eq!(col_age().asc_nulls_last().render(), "age ASC NULLS LAST");
534 }
535
536 #[test]
537 fn render_order_desc_nulls_first() {
538 assert_eq!(col_age().desc_nulls_first().render(), "age DESC NULLS FIRST");
539 }
540
541 #[test]
544 fn render_is_null_no_params() {
545 let (_, params) = col_email().is_null().render();
546 assert!(params.is_empty(), "IS NULL should not produce parameters");
547 }
548
549 #[test]
550 fn render_and_with_null() {
551 let (sql, params) = col_age().gt(25).and(col_email().is_null()).render();
553 assert_eq!(sql, "(age > $1 AND email IS NULL)");
554 assert_eq!(params.len(), 1); }
556}