1use serde_json::Value as JsonValue;
9
10#[derive(Debug, Clone)]
12pub enum SqlValue {
13 Int(i64),
14 Float(f64),
15 Text(String),
16 Bool(bool),
17 Null,
18 Json(serde_json::Value),
19 Bytes(Vec<u8>),
20}
21
22impl From<i64> for SqlValue {
23 fn from(v: i64) -> Self {
24 SqlValue::Int(v)
25 }
26}
27impl From<i32> for SqlValue {
28 fn from(v: i32) -> Self {
29 SqlValue::Int(v as i64)
30 }
31}
32impl From<u32> for SqlValue {
33 fn from(v: u32) -> Self {
34 SqlValue::Int(v as i64)
35 }
36}
37impl From<f64> for SqlValue {
38 fn from(v: f64) -> Self {
39 SqlValue::Float(v)
40 }
41}
42impl From<String> for SqlValue {
43 fn from(v: String) -> Self {
44 SqlValue::Text(v)
45 }
46}
47impl From<&str> for SqlValue {
48 fn from(v: &str) -> Self {
49 SqlValue::Text(v.to_string())
50 }
51}
52impl From<bool> for SqlValue {
53 fn from(v: bool) -> Self {
54 SqlValue::Bool(v)
55 }
56}
57impl From<serde_json::Value> for SqlValue {
58 fn from(v: serde_json::Value) -> Self {
59 SqlValue::Json(v)
60 }
61}
62
63#[derive(Debug, Clone)]
66pub struct FilterExpr {
67 pub sql: String,
69 pub bindings: Vec<SqlValue>,
71}
72
73impl FilterExpr {
74 pub fn new(sql: impl Into<String>, bindings: Vec<SqlValue>) -> Self {
75 Self {
76 sql: sql.into(),
77 bindings,
78 }
79 }
80
81 pub fn raw(sql: impl Into<String>) -> Self {
82 Self {
83 sql: sql.into(),
84 bindings: vec![],
85 }
86 }
87
88 pub fn and(self, other: FilterExpr) -> FilterExpr {
90 FilterExpr {
91 sql: format!("({}) AND ({})", self.sql, other.sql),
92 bindings: [self.bindings, other.bindings].concat(),
93 }
94 }
95
96 pub fn or(self, other: FilterExpr) -> FilterExpr {
98 FilterExpr {
99 sql: format!("({}) OR ({})", self.sql, other.sql),
100 bindings: [self.bindings, other.bindings].concat(),
101 }
102 }
103
104 pub fn not(self) -> FilterExpr {
106 FilterExpr {
107 sql: format!("NOT ({})", self.sql),
108 bindings: self.bindings,
109 }
110 }
111}
112
113pub fn reindex_params(sql: &str, offset: usize) -> String {
115 if offset == 0 {
116 return sql.to_string();
117 }
118 let mut out = String::with_capacity(sql.len() + 8);
119 let bytes = sql.as_bytes();
120 let mut i = 0;
121 while i < bytes.len() {
122 if bytes[i] == b'$' {
123 let start = i + 1;
124 let mut end = start;
125 while end < bytes.len() && bytes[end].is_ascii_digit() {
126 end += 1;
127 }
128 if end > start {
129 let num: usize = sql[start..end].parse().unwrap_or(0);
130 out.push('$');
131 out.push_str(&(num + offset).to_string());
132 i = end;
133 continue;
134 }
135 }
136 out.push(bytes[i] as char);
137 i += 1;
138 }
139 out
140}
141
142#[derive(Debug, Clone)]
148pub struct ColumnExpr {
149 pub col: String,
151}
152
153impl ColumnExpr {
154 pub fn new(col: impl Into<String>) -> Self {
155 Self { col: col.into() }
156 }
157
158 fn placeholder(&self, idx: usize) -> String {
159 format!("${}", idx)
160 }
161
162 pub fn eq<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
165 FilterExpr::new(format!("\"{}\" = $1", self.col), vec![val.into()])
166 }
167
168 pub fn ne<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
169 FilterExpr::new(format!("\"{}\" != $1", self.col), vec![val.into()])
170 }
171
172 pub fn gt<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
173 FilterExpr::new(format!("\"{}\" > $1", self.col), vec![val.into()])
174 }
175
176 pub fn gte<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
177 FilterExpr::new(format!("\"{}\" >= $1", self.col), vec![val.into()])
178 }
179
180 pub fn lt<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
181 FilterExpr::new(format!("\"{}\" < $1", self.col), vec![val.into()])
182 }
183
184 pub fn lte<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
185 FilterExpr::new(format!("\"{}\" <= $1", self.col), vec![val.into()])
186 }
187
188 pub fn like(&self, pattern: impl Into<String>) -> FilterExpr {
191 FilterExpr::new(
192 format!("\"{}\" LIKE $1", self.col),
193 vec![SqlValue::Text(pattern.into())],
194 )
195 }
196
197 pub fn ilike(&self, pattern: impl Into<String>) -> FilterExpr {
198 FilterExpr::new(
199 format!("\"{}\" ILIKE $1", self.col),
200 vec![SqlValue::Text(pattern.into())],
201 )
202 }
203
204 pub fn starts_with(&self, prefix: impl Into<String>) -> FilterExpr {
205 let p = format!("{}%", prefix.into());
206 FilterExpr::new(
207 format!("\"{}\" ILIKE $1", self.col),
208 vec![SqlValue::Text(p)],
209 )
210 }
211
212 pub fn ends_with(&self, suffix: impl Into<String>) -> FilterExpr {
213 let s = format!("%{}", suffix.into());
214 FilterExpr::new(
215 format!("\"{}\" ILIKE $1", self.col),
216 vec![SqlValue::Text(s)],
217 )
218 }
219
220 pub fn contains(&self, substr: impl Into<String>) -> FilterExpr {
221 let s = format!("%{}%", substr.into());
222 FilterExpr::new(
223 format!("\"{}\" ILIKE $1", self.col),
224 vec![SqlValue::Text(s)],
225 )
226 }
227
228 pub fn matches_regex(&self, pattern: impl Into<String>) -> FilterExpr {
229 FilterExpr::new(
230 format!("\"{}\" ~ $1", self.col),
231 vec![SqlValue::Text(pattern.into())],
232 )
233 }
234
235 pub fn in_<V: Into<SqlValue> + Clone>(
238 &self,
239 values: impl IntoIterator<Item = V>,
240 ) -> FilterExpr {
241 let vals: Vec<SqlValue> = values.into_iter().map(|v| v.into()).collect();
242 if vals.is_empty() {
243 return FilterExpr::raw("FALSE");
244 }
245 let placeholders: Vec<String> = (1..=vals.len()).map(|i| format!("${}", i)).collect();
246 FilterExpr::new(
247 format!("\"{}\" IN ({})", self.col, placeholders.join(", ")),
248 vals,
249 )
250 }
251
252 pub fn not_in<V: Into<SqlValue>>(&self, values: impl IntoIterator<Item = V>) -> FilterExpr {
253 let vals: Vec<SqlValue> = values.into_iter().map(|v| v.into()).collect();
254 if vals.is_empty() {
255 return FilterExpr::raw("TRUE");
256 }
257 let placeholders: Vec<String> = (1..=vals.len()).map(|i| format!("${}", i)).collect();
258 FilterExpr::new(
259 format!("\"{}\" NOT IN ({})", self.col, placeholders.join(", ")),
260 vals,
261 )
262 }
263
264 pub fn between<V: Into<SqlValue>>(&self, low: V, high: V) -> FilterExpr {
265 FilterExpr::new(
266 format!("\"{}\" BETWEEN $1 AND $2", self.col),
267 vec![low.into(), high.into()],
268 )
269 }
270
271 pub fn is_null(&self) -> FilterExpr {
274 FilterExpr::raw(format!("\"{}\" IS NULL", self.col))
275 }
276
277 pub fn is_not_null(&self) -> FilterExpr {
278 FilterExpr::raw(format!("\"{}\" IS NOT NULL", self.col))
279 }
280
281 pub fn is_true(&self) -> FilterExpr {
284 FilterExpr::raw(format!("\"{}\" = TRUE", self.col))
285 }
286
287 pub fn is_false(&self) -> FilterExpr {
288 FilterExpr::raw(format!("\"{}\" = FALSE", self.col))
289 }
290
291 pub fn before<V: Into<SqlValue>>(&self, date: V) -> FilterExpr {
294 FilterExpr::new(format!("\"{}\" < $1", self.col), vec![date.into()])
295 }
296
297 pub fn after<V: Into<SqlValue>>(&self, date: V) -> FilterExpr {
298 FilterExpr::new(format!("\"{}\" > $1", self.col), vec![date.into()])
299 }
300
301 pub fn in_last(&self, n: u32, unit: TimeUnit) -> FilterExpr {
303 let interval = format!("{} {}", n, unit.as_str());
304 FilterExpr::raw(format!(
305 "\"{}\" > NOW() - INTERVAL '{}'",
306 self.col, interval
307 ))
308 }
309
310 pub fn this_week(&self) -> FilterExpr {
311 FilterExpr::raw(format!(
312 "date_trunc('week', \"{}\") = date_trunc('week', NOW())",
313 self.col
314 ))
315 }
316
317 pub fn this_month(&self) -> FilterExpr {
318 FilterExpr::raw(format!(
319 "date_trunc('month', \"{}\") = date_trunc('month', NOW())",
320 self.col
321 ))
322 }
323
324 pub fn this_year(&self) -> FilterExpr {
325 FilterExpr::raw(format!(
326 "date_trunc('year', \"{}\") = date_trunc('year', NOW())",
327 self.col
328 ))
329 }
330
331 pub fn json_eq(&self, key: &str, val: impl Into<String>) -> FilterExpr {
334 FilterExpr::new(
335 format!("\"{}\"->>'{}' = $1", self.col, key),
336 vec![SqlValue::Text(val.into())],
337 )
338 }
339
340 pub fn json_has_key(&self, key: &str) -> FilterExpr {
341 FilterExpr::new(
342 format!("\"{}\" ? $1", self.col),
343 vec![SqlValue::Text(key.to_string())],
344 )
345 }
346
347 pub fn json_contains(&self, val: serde_json::Value) -> FilterExpr {
348 FilterExpr::new(
349 format!("\"{}\" @> $1::jsonb", self.col),
350 vec![SqlValue::Json(val)],
351 )
352 }
353
354 pub fn asc(&self) -> OrderExpr {
357 OrderExpr {
358 sql: format!("\"{}\" ASC", self.col),
359 }
360 }
361
362 pub fn desc(&self) -> OrderExpr {
363 OrderExpr {
364 sql: format!("\"{}\" DESC", self.col),
365 }
366 }
367
368 pub fn expr(&self) -> String {
370 format!("\"{}\"", self.col)
371 }
372}
373
374#[derive(Debug, Clone)]
379pub struct OrderExpr {
380 pub sql: String,
381}
382
383impl OrderExpr {
384 pub fn nulls_last(self) -> Self {
385 Self {
386 sql: format!("{} NULLS LAST", self.sql),
387 }
388 }
389
390 pub fn nulls_first(self) -> Self {
391 Self {
392 sql: format!("{} NULLS FIRST", self.sql),
393 }
394 }
395
396 pub fn then(self, other: OrderExpr) -> Self {
397 Self {
398 sql: format!("{}, {}", self.sql, other.sql),
399 }
400 }
401}
402
403#[derive(Debug, Clone, Copy)]
408pub enum TimeUnit {
409 Seconds,
410 Minutes,
411 Hours,
412 Days,
413 Weeks,
414 Months,
415 Years,
416}
417
418impl TimeUnit {
419 pub fn as_str(self) -> &'static str {
420 match self {
421 TimeUnit::Seconds => "seconds",
422 TimeUnit::Minutes => "minutes",
423 TimeUnit::Hours => "hours",
424 TimeUnit::Days => "days",
425 TimeUnit::Weeks => "weeks",
426 TimeUnit::Months => "months",
427 TimeUnit::Years => "years",
428 }
429 }
430}