postrust_sql/
expr.rs

1//! SQL expression building.
2
3use crate::{builder::SqlFragment, identifier::escape_ident, param::SqlParam};
4
5/// A SQL expression (for WHERE, HAVING, etc.).
6#[derive(Clone, Debug)]
7pub struct Expr {
8    fragment: SqlFragment,
9}
10
11impl Expr {
12    /// Create an expression from a SQL fragment.
13    pub fn from_fragment(fragment: SqlFragment) -> Self {
14        Self { fragment }
15    }
16
17    /// Create a column reference expression.
18    pub fn column(name: &str) -> Self {
19        Self {
20            fragment: SqlFragment::raw(escape_ident(name)),
21        }
22    }
23
24    /// Create a qualified column reference (table.column).
25    pub fn qualified_column(table: &str, column: &str) -> Self {
26        Self {
27            fragment: SqlFragment::raw(format!(
28                "{}.{}",
29                escape_ident(table),
30                escape_ident(column)
31            )),
32        }
33    }
34
35    /// Create an equality expression: column = $1
36    pub fn eq(column: &str, value: impl Into<SqlParam>) -> Self {
37        let mut frag = SqlFragment::new();
38        frag.push(&escape_ident(column));
39        frag.push(" = ");
40        frag.push_param(value);
41        Self { fragment: frag }
42    }
43
44    /// Create a not-equal expression: column <> $1
45    pub fn neq(column: &str, value: impl Into<SqlParam>) -> Self {
46        let mut frag = SqlFragment::new();
47        frag.push(&escape_ident(column));
48        frag.push(" <> ");
49        frag.push_param(value);
50        Self { fragment: frag }
51    }
52
53    /// Create a greater-than expression: column > $1
54    pub fn gt(column: &str, value: impl Into<SqlParam>) -> Self {
55        let mut frag = SqlFragment::new();
56        frag.push(&escape_ident(column));
57        frag.push(" > ");
58        frag.push_param(value);
59        Self { fragment: frag }
60    }
61
62    /// Create a greater-than-or-equal expression: column >= $1
63    pub fn gte(column: &str, value: impl Into<SqlParam>) -> Self {
64        let mut frag = SqlFragment::new();
65        frag.push(&escape_ident(column));
66        frag.push(" >= ");
67        frag.push_param(value);
68        Self { fragment: frag }
69    }
70
71    /// Create a less-than expression: column < $1
72    pub fn lt(column: &str, value: impl Into<SqlParam>) -> Self {
73        let mut frag = SqlFragment::new();
74        frag.push(&escape_ident(column));
75        frag.push(" < ");
76        frag.push_param(value);
77        Self { fragment: frag }
78    }
79
80    /// Create a less-than-or-equal expression: column <= $1
81    pub fn lte(column: &str, value: impl Into<SqlParam>) -> Self {
82        let mut frag = SqlFragment::new();
83        frag.push(&escape_ident(column));
84        frag.push(" <= ");
85        frag.push_param(value);
86        Self { fragment: frag }
87    }
88
89    /// Create a LIKE expression: column LIKE $1
90    pub fn like(column: &str, pattern: impl Into<SqlParam>) -> Self {
91        let mut frag = SqlFragment::new();
92        frag.push(&escape_ident(column));
93        frag.push(" LIKE ");
94        frag.push_param(pattern);
95        Self { fragment: frag }
96    }
97
98    /// Create an ILIKE expression: column ILIKE $1
99    pub fn ilike(column: &str, pattern: impl Into<SqlParam>) -> Self {
100        let mut frag = SqlFragment::new();
101        frag.push(&escape_ident(column));
102        frag.push(" ILIKE ");
103        frag.push_param(pattern);
104        Self { fragment: frag }
105    }
106
107    /// Create an IS NULL expression: column IS NULL
108    pub fn is_null(column: &str) -> Self {
109        Self {
110            fragment: SqlFragment::raw(format!("{} IS NULL", escape_ident(column))),
111        }
112    }
113
114    /// Create an IS NOT NULL expression: column IS NOT NULL
115    pub fn is_not_null(column: &str) -> Self {
116        Self {
117            fragment: SqlFragment::raw(format!("{} IS NOT NULL", escape_ident(column))),
118        }
119    }
120
121    /// Create an IN expression: column IN ($1, $2, ...)
122    pub fn in_list(column: &str, values: Vec<SqlParam>) -> Self {
123        if values.is_empty() {
124            return Self {
125                fragment: SqlFragment::raw("FALSE"),
126            };
127        }
128
129        let mut frag = SqlFragment::new();
130        frag.push(&escape_ident(column));
131        frag.push(" IN (");
132
133        for (i, value) in values.into_iter().enumerate() {
134            if i > 0 {
135                frag.push(", ");
136            }
137            frag.push_param(value);
138        }
139
140        frag.push(")");
141        Self { fragment: frag }
142    }
143
144    /// Create a contains expression: column @> $1
145    pub fn contains(column: &str, value: impl Into<SqlParam>) -> Self {
146        let mut frag = SqlFragment::new();
147        frag.push(&escape_ident(column));
148        frag.push(" @> ");
149        frag.push_param(value);
150        Self { fragment: frag }
151    }
152
153    /// Create a contained-by expression: column <@ $1
154    pub fn contained_by(column: &str, value: impl Into<SqlParam>) -> Self {
155        let mut frag = SqlFragment::new();
156        frag.push(&escape_ident(column));
157        frag.push(" <@ ");
158        frag.push_param(value);
159        Self { fragment: frag }
160    }
161
162    /// Create an overlap expression: column && $1
163    pub fn overlaps(column: &str, value: impl Into<SqlParam>) -> Self {
164        let mut frag = SqlFragment::new();
165        frag.push(&escape_ident(column));
166        frag.push(" && ");
167        frag.push_param(value);
168        Self { fragment: frag }
169    }
170
171    /// Create a full-text search expression: column @@ to_tsquery($1)
172    pub fn fts(column: &str, query: impl Into<SqlParam>, language: Option<&str>) -> Self {
173        let mut frag = SqlFragment::new();
174        frag.push(&escape_ident(column));
175        frag.push(" @@ ");
176
177        if let Some(lang) = language {
178            frag.push("to_tsquery(");
179            frag.push_param(lang);
180            frag.push(", ");
181            frag.push_param(query);
182            frag.push(")");
183        } else {
184            frag.push("to_tsquery(");
185            frag.push_param(query);
186            frag.push(")");
187        }
188
189        Self { fragment: frag }
190    }
191
192    /// Negate this expression: NOT (expr)
193    pub fn not(self) -> Self {
194        let mut frag = SqlFragment::raw("NOT ");
195        frag.append(self.fragment.parens());
196        Self { fragment: frag }
197    }
198
199    /// Combine with AND: self AND other
200    pub fn and(self, other: Expr) -> Self {
201        let mut frag = self.fragment.parens();
202        frag.push(" AND ");
203        frag.append(other.fragment.parens());
204        Self { fragment: frag }
205    }
206
207    /// Combine with OR: self OR other
208    pub fn or(self, other: Expr) -> Self {
209        let mut frag = self.fragment.parens();
210        frag.push(" OR ");
211        frag.append(other.fragment.parens());
212        Self { fragment: frag }
213    }
214
215    /// Combine multiple expressions with AND.
216    pub fn and_all(exprs: impl IntoIterator<Item = Expr>) -> Self {
217        let frags: Vec<_> = exprs.into_iter().map(|e| e.fragment.parens()).collect();
218        if frags.is_empty() {
219            return Self {
220                fragment: SqlFragment::raw("TRUE"),
221            };
222        }
223        Self {
224            fragment: SqlFragment::join(" AND ", frags),
225        }
226    }
227
228    /// Combine multiple expressions with OR.
229    pub fn or_all(exprs: impl IntoIterator<Item = Expr>) -> Self {
230        let frags: Vec<_> = exprs.into_iter().map(|e| e.fragment.parens()).collect();
231        if frags.is_empty() {
232            return Self {
233                fragment: SqlFragment::raw("FALSE"),
234            };
235        }
236        Self {
237            fragment: SqlFragment::join(" OR ", frags),
238        }
239    }
240
241    /// Convert to a SQL fragment.
242    pub fn into_fragment(self) -> SqlFragment {
243        self.fragment
244    }
245
246    /// Get the SQL string.
247    pub fn sql(&self) -> &str {
248        self.fragment.sql()
249    }
250
251    /// Get the parameters.
252    pub fn params(&self) -> &[SqlParam] {
253        self.fragment.params()
254    }
255}
256
257/// ORDER BY expression.
258#[derive(Clone, Debug)]
259pub struct OrderExpr {
260    column: String,
261    direction: Option<OrderDirection>,
262    nulls: Option<NullsOrder>,
263}
264
265#[derive(Clone, Debug, PartialEq)]
266pub enum OrderDirection {
267    Asc,
268    Desc,
269}
270
271#[derive(Clone, Debug, PartialEq)]
272pub enum NullsOrder {
273    First,
274    Last,
275}
276
277impl OrderExpr {
278    /// Create a new ORDER BY expression.
279    pub fn new(column: impl Into<String>) -> Self {
280        Self {
281            column: column.into(),
282            direction: None,
283            nulls: None,
284        }
285    }
286
287    /// Set ascending order.
288    pub fn asc(mut self) -> Self {
289        self.direction = Some(OrderDirection::Asc);
290        self
291    }
292
293    /// Set descending order.
294    pub fn desc(mut self) -> Self {
295        self.direction = Some(OrderDirection::Desc);
296        self
297    }
298
299    /// Set NULLS FIRST.
300    pub fn nulls_first(mut self) -> Self {
301        self.nulls = Some(NullsOrder::First);
302        self
303    }
304
305    /// Set NULLS LAST.
306    pub fn nulls_last(mut self) -> Self {
307        self.nulls = Some(NullsOrder::Last);
308        self
309    }
310
311    /// Convert to SQL fragment.
312    pub fn into_fragment(self) -> SqlFragment {
313        let mut frag = SqlFragment::raw(escape_ident(&self.column));
314
315        if let Some(dir) = self.direction {
316            match dir {
317                OrderDirection::Asc => frag.push(" ASC"),
318                OrderDirection::Desc => frag.push(" DESC"),
319            };
320        }
321
322        if let Some(nulls) = self.nulls {
323            match nulls {
324                NullsOrder::First => frag.push(" NULLS FIRST"),
325                NullsOrder::Last => frag.push(" NULLS LAST"),
326            };
327        }
328
329        frag
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_expr_eq() {
339        let expr = Expr::eq("name", "John");
340        assert_eq!(expr.sql(), "\"name\" = $1");
341        assert_eq!(expr.params().len(), 1);
342    }
343
344    #[test]
345    fn test_expr_in_list() {
346        let expr = Expr::in_list(
347            "id",
348            vec![SqlParam::Int(1), SqlParam::Int(2), SqlParam::Int(3)],
349        );
350        assert_eq!(expr.sql(), "\"id\" IN ($1, $2, $3)");
351        assert_eq!(expr.params().len(), 3);
352    }
353
354    #[test]
355    fn test_expr_is_null() {
356        let expr = Expr::is_null("deleted_at");
357        assert_eq!(expr.sql(), "\"deleted_at\" IS NULL");
358    }
359
360    #[test]
361    fn test_expr_and() {
362        let expr1 = Expr::eq("a", 1i64);
363        let expr2 = Expr::eq("b", 2i64);
364        let combined = expr1.and(expr2);
365
366        assert!(combined.sql().contains(" AND "));
367        assert_eq!(combined.params().len(), 2);
368    }
369
370    #[test]
371    fn test_expr_or() {
372        let expr1 = Expr::eq("a", 1i64);
373        let expr2 = Expr::eq("b", 2i64);
374        let combined = expr1.or(expr2);
375
376        assert!(combined.sql().contains(" OR "));
377    }
378
379    #[test]
380    fn test_expr_not() {
381        let expr = Expr::eq("active", true).not();
382        assert!(expr.sql().starts_with("NOT"));
383    }
384
385    #[test]
386    fn test_order_expr() {
387        let order = OrderExpr::new("created_at").desc().nulls_last();
388        let frag = order.into_fragment();
389        assert_eq!(frag.sql(), "\"created_at\" DESC NULLS LAST");
390    }
391}