1use crate::{builder::SqlFragment, identifier::escape_ident, param::SqlParam};
4
5#[derive(Clone, Debug)]
7pub struct Expr {
8 fragment: SqlFragment,
9}
10
11impl Expr {
12 pub fn from_fragment(fragment: SqlFragment) -> Self {
14 Self { fragment }
15 }
16
17 pub fn column(name: &str) -> Self {
19 Self {
20 fragment: SqlFragment::raw(escape_ident(name)),
21 }
22 }
23
24 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 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 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 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 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 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 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 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 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 pub fn is_null(column: &str) -> Self {
105 Self {
106 fragment: SqlFragment::raw(format!("{} IS NULL", escape_ident(column))),
107 }
108 }
109
110 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 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 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 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 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 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 #[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 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 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 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 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 pub fn into_fragment(self) -> SqlFragment {
241 self.fragment
242 }
243
244 pub fn sql(&self) -> &str {
246 self.fragment.sql()
247 }
248
249 pub fn params(&self) -> &[SqlParam] {
251 self.fragment.params()
252 }
253}
254
255#[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 pub fn new(column: impl Into<String>) -> Self {
278 Self {
279 column: column.into(),
280 direction: None,
281 nulls: None,
282 }
283 }
284
285 pub fn asc(mut self) -> Self {
287 self.direction = Some(OrderDirection::Asc);
288 self
289 }
290
291 pub fn desc(mut self) -> Self {
293 self.direction = Some(OrderDirection::Desc);
294 self
295 }
296
297 pub fn nulls_first(mut self) -> Self {
299 self.nulls = Some(NullsOrder::First);
300 self
301 }
302
303 pub fn nulls_last(mut self) -> Self {
305 self.nulls = Some(NullsOrder::Last);
306 self
307 }
308
309 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}