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!(
28 "{}.{}",
29 escape_ident(table),
30 escape_ident(column)
31 )),
32 }
33 }
34
35 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 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 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 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 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 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 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 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 pub fn is_null(column: &str) -> Self {
109 Self {
110 fragment: SqlFragment::raw(format!("{} IS NULL", escape_ident(column))),
111 }
112 }
113
114 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 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 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 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 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 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 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 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 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 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 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 pub fn into_fragment(self) -> SqlFragment {
243 self.fragment
244 }
245
246 pub fn sql(&self) -> &str {
248 self.fragment.sql()
249 }
250
251 pub fn params(&self) -> &[SqlParam] {
253 self.fragment.params()
254 }
255}
256
257#[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 pub fn new(column: impl Into<String>) -> Self {
280 Self {
281 column: column.into(),
282 direction: None,
283 nulls: None,
284 }
285 }
286
287 pub fn asc(mut self) -> Self {
289 self.direction = Some(OrderDirection::Asc);
290 self
291 }
292
293 pub fn desc(mut self) -> Self {
295 self.direction = Some(OrderDirection::Desc);
296 self
297 }
298
299 pub fn nulls_first(mut self) -> Self {
301 self.nulls = Some(NullsOrder::First);
302 self
303 }
304
305 pub fn nulls_last(mut self) -> Self {
307 self.nulls = Some(NullsOrder::Last);
308 self
309 }
310
311 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}