Skip to main content

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