Skip to main content

rustorm_core/
column.rs

1//! Типизированный DSL для построения фильтров запроса.
2//!
3//! При вызове `.filter(|u| u.email.eq("foo"))` `u` — это экземпляр
4//! сгенерированной макросом структуры `<Model>Columns`, все поля которой
5//! имеют тип `ColumnExpr`. `ColumnExpr` реализует методы сравнения и
6//! возвращает `FilterExpr`, которые QueryBuilder собирает в WHERE-клаузу.
7
8use serde_json::Value as JsonValue;
9
10/// Обёртка вокруг SQL-значения, которое можно передать как bind-параметр.
11#[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/// Один законченный SQL-фрагмент с bind-параметрами.
64/// Параметры используют $1, $2 ... — positional placeholder'ы PostgreSQL.
65#[derive(Debug, Clone)]
66pub struct FilterExpr {
67    /// Фрагмент SQL вида `"column" = $N`
68    pub sql: String,
69    /// Значения для bind
70    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    /// AND комбинация двух выражений
89    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    /// OR комбинация двух выражений
97    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    /// Отрицание
105    pub fn not(self) -> FilterExpr {
106        FilterExpr {
107            sql: format!("NOT ({})", self.sql),
108            bindings: self.bindings,
109        }
110    }
111}
112
113/// Перепривязывает параметры: смещает $1,$2... на `offset`.
114pub 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// ---------------------------------------------------------------------------
143// ColumnExpr — одно поле таблицы, с методами фильтрации
144// ---------------------------------------------------------------------------
145
146/// Представляет одно поле таблицы в контексте построения запроса.
147#[derive(Debug, Clone)]
148pub struct ColumnExpr {
149    /// Имя колонки (как в БД), напр. `"email"`
150    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    // ── Сравнение ───────────────────────────────────────────────────────────
163
164    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    // ── Строки ──────────────────────────────────────────────────────────────
189
190    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    // ── Коллекции ───────────────────────────────────────────────────────────
236
237    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    // ── NULL ────────────────────────────────────────────────────────────────
272
273    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    // ── Boolean ─────────────────────────────────────────────────────────────
282
283    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    // ── Даты ────────────────────────────────────────────────────────────────
292
293    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    /// `col > NOW() - INTERVAL 'N unit'`
302    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    // ── JSON (PostgreSQL JSONB) ──────────────────────────────────────────────
332
333    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    // ── ORDER BY helpers ────────────────────────────────────────────────────
355
356    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    /// Возвращает выражение для SELECT списка
369    pub fn expr(&self) -> String {
370        format!("\"{}\"", self.col)
371    }
372}
373
374// ---------------------------------------------------------------------------
375// OrderExpr
376// ---------------------------------------------------------------------------
377
378#[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// ---------------------------------------------------------------------------
404// TimeUnit
405// ---------------------------------------------------------------------------
406
407#[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}