Skip to main content

rok_orm_core/
condition.rs

1//! SQL value and condition types.
2
3use std::fmt;
4
5// ── SqlValue ─────────────────────────────────────────────────────────────────
6
7/// A typed SQL parameter value.
8#[derive(Debug, Clone, PartialEq)]
9#[non_exhaustive]
10pub enum SqlValue {
11    Text(String),
12    Integer(i64),
13    Float(f64),
14    Bool(bool),
15    Null,
16}
17
18impl SqlValue {
19    /// Render as a SQL literal (for display / debug — not safe for user input).
20    pub fn to_sql_literal(&self) -> String {
21        match self {
22            Self::Text(s) => format!("'{}'", s.replace('\'', "''")),
23            Self::Integer(n) => n.to_string(),
24            Self::Float(f) => f.to_string(),
25            Self::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
26            Self::Null => "NULL".to_string(),
27        }
28    }
29}
30
31impl fmt::Display for SqlValue {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        f.write_str(&self.to_sql_literal())
34    }
35}
36
37impl From<&str> for SqlValue {
38    fn from(s: &str) -> Self {
39        Self::Text(s.to_string())
40    }
41}
42impl From<String> for SqlValue {
43    fn from(s: String) -> Self {
44        Self::Text(s)
45    }
46}
47impl From<i8> for SqlValue {
48    fn from(n: i8) -> Self {
49        Self::Integer(n as i64)
50    }
51}
52impl From<i16> for SqlValue {
53    fn from(n: i16) -> Self {
54        Self::Integer(n as i64)
55    }
56}
57impl From<i32> for SqlValue {
58    fn from(n: i32) -> Self {
59        Self::Integer(n as i64)
60    }
61}
62impl From<i64> for SqlValue {
63    fn from(n: i64) -> Self {
64        Self::Integer(n)
65    }
66}
67impl From<u32> for SqlValue {
68    fn from(n: u32) -> Self {
69        Self::Integer(n as i64)
70    }
71}
72impl From<u64> for SqlValue {
73    fn from(n: u64) -> Self {
74        Self::Integer(n as i64)
75    }
76}
77impl From<f32> for SqlValue {
78    fn from(f: f32) -> Self {
79        Self::Float(f as f64)
80    }
81}
82impl From<f64> for SqlValue {
83    fn from(f: f64) -> Self {
84        Self::Float(f)
85    }
86}
87impl From<bool> for SqlValue {
88    fn from(b: bool) -> Self {
89        Self::Bool(b)
90    }
91}
92impl<T: Into<SqlValue>> From<Option<T>> for SqlValue {
93    fn from(opt: Option<T>) -> Self {
94        match opt {
95            Some(v) => v.into(),
96            None => Self::Null,
97        }
98    }
99}
100
101// ── JoinOp ───────────────────────────────────────────────────────────────────
102
103/// The logical operator used to join a condition to the preceding clause.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum JoinOp {
106    And,
107    Or,
108}
109
110impl fmt::Display for JoinOp {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            Self::And => write!(f, "AND"),
114            Self::Or => write!(f, "OR"),
115        }
116    }
117}
118
119// ── Condition ────────────────────────────────────────────────────────────────
120
121/// A single WHERE clause condition.
122#[derive(Debug, Clone)]
123#[non_exhaustive]
124pub enum Condition {
125    Eq(String, SqlValue),
126    Ne(String, SqlValue),
127    Gt(String, SqlValue),
128    Gte(String, SqlValue),
129    Lt(String, SqlValue),
130    Lte(String, SqlValue),
131    Like(String, String),
132    NotLike(String, String),
133    ILike(String, String),
134    IsNull(String),
135    IsNotNull(String),
136    In(String, Vec<SqlValue>),
137    NotIn(String, Vec<SqlValue>),
138    Between(String, SqlValue, SqlValue),
139    NotBetween(String, SqlValue, SqlValue),
140    Raw(String),
141    /// Grouped sub-conditions joined by their own `JoinOp`.  Renders as `(cond1 AND cond2 OR …)`.
142    Group(Vec<(JoinOp, Condition)>),
143    /// JSONB text extraction: `col->>'key' = $N` (PostgreSQL) or `json_extract(col,'$.key') = ?` (SQLite).
144    JsonGet(String, String, SqlValue),
145    /// `[NOT] EXISTS (SELECT 1 FROM table WHERE fk_expr [AND inner_conds])`.
146    /// Used by `has()`, `doesnt_have()`, `where_has()`, and `where_doesnt_have()`.
147    Subquery {
148        exists: bool,
149        table: String,
150        fk_expr: String,
151        inner: Vec<(JoinOp, Condition)>,
152    },
153}
154
155impl Condition {
156    /// Render as a SQL fragment using positional placeholders (`$N`).
157    ///
158    /// `offset` is the next available parameter index (1-based).
159    /// Returns `(sql_fragment, collected_params)`.
160    ///
161    /// For SQLite (`?` placeholders) use [`to_param_sql_sqlite`](Self::to_param_sql_sqlite).
162    pub fn to_param_sql(&self, offset: usize) -> (String, Vec<SqlValue>) {
163        match self {
164            Self::Eq(col, v) => (format!("{col} = ${offset}"), vec![v.clone()]),
165            Self::Ne(col, v) => (format!("{col} != ${offset}"), vec![v.clone()]),
166            Self::Gt(col, v) => (format!("{col} > ${offset}"), vec![v.clone()]),
167            Self::Gte(col, v) => (format!("{col} >= ${offset}"), vec![v.clone()]),
168            Self::Lt(col, v) => (format!("{col} < ${offset}"), vec![v.clone()]),
169            Self::Lte(col, v) => (format!("{col} <= ${offset}"), vec![v.clone()]),
170            Self::Like(col, pat) => (
171                format!("{col} LIKE ${offset}"),
172                vec![SqlValue::Text(pat.clone())],
173            ),
174            Self::NotLike(col, pat) => (
175                format!("{col} NOT LIKE ${offset}"),
176                vec![SqlValue::Text(pat.clone())],
177            ),
178            Self::ILike(col, pat) => (
179                format!("{col} ILIKE ${offset}"),
180                vec![SqlValue::Text(pat.clone())],
181            ),
182            Self::IsNull(col) => (format!("{col} IS NULL"), vec![]),
183            Self::IsNotNull(col) => (format!("{col} IS NOT NULL"), vec![]),
184            Self::In(col, vals) => {
185                let placeholders: Vec<String> = vals
186                    .iter()
187                    .enumerate()
188                    .map(|(i, _)| format!("${}", offset + i))
189                    .collect();
190                (
191                    format!("{col} IN ({})", placeholders.join(", ")),
192                    vals.clone(),
193                )
194            }
195            Self::NotIn(col, vals) => {
196                let placeholders: Vec<String> = vals
197                    .iter()
198                    .enumerate()
199                    .map(|(i, _)| format!("${}", offset + i))
200                    .collect();
201                (
202                    format!("{col} NOT IN ({})", placeholders.join(", ")),
203                    vals.clone(),
204                )
205            }
206            Self::Between(col, lo, hi) => (
207                format!("{col} BETWEEN ${offset} AND ${}", offset + 1),
208                vec![lo.clone(), hi.clone()],
209            ),
210            Self::NotBetween(col, lo, hi) => (
211                format!("{col} NOT BETWEEN ${offset} AND ${}", offset + 1),
212                vec![lo.clone(), hi.clone()],
213            ),
214            Self::Raw(sql) => (sql.clone(), vec![]),
215            Self::Group(inner) => {
216                let mut parts: Vec<String> = Vec::new();
217                let mut group_params: Vec<SqlValue> = Vec::new();
218                for (idx, (op, cond)) in inner.iter().enumerate() {
219                    let (frag, ps) = cond.to_param_sql(offset + group_params.len());
220                    group_params.extend(ps);
221                    if idx > 0 {
222                        parts.push(format!("{op} {frag}"));
223                    } else {
224                        parts.push(frag);
225                    }
226                }
227                (format!("({})", parts.join(" ")), group_params)
228            }
229            Self::JsonGet(col, key, val) => (
230                format!("{col}->>'{}' = ${offset}", key.replace('\'', "''")),
231                vec![val.clone()],
232            ),
233            Self::Subquery {
234                exists,
235                table,
236                fk_expr,
237                inner,
238            } => {
239                let kw = if *exists { "EXISTS" } else { "NOT EXISTS" };
240                let mut sub_params: Vec<SqlValue> = Vec::new();
241                let mut parts: Vec<String> = Vec::new();
242                for (i, (op, cond)) in inner.iter().enumerate() {
243                    let (frag, ps) = cond.to_param_sql(offset + sub_params.len());
244                    sub_params.extend(ps);
245                    parts.push(if i == 0 {
246                        format!("AND {frag}")
247                    } else {
248                        format!("{op} {frag}")
249                    });
250                }
251                let extra = if parts.is_empty() {
252                    String::new()
253                } else {
254                    format!(" {}", parts.join(" "))
255                };
256                (
257                    format!("{kw} (SELECT 1 FROM {table} WHERE {fk_expr}{extra})"),
258                    sub_params,
259                )
260            }
261        }
262    }
263
264    /// Render as a SQL fragment using anonymous `?` placeholders (SQLite dialect).
265    ///
266    /// Returns `(sql_fragment, collected_params)`.
267    pub fn to_param_sql_sqlite(&self) -> (String, Vec<SqlValue>) {
268        match self {
269            Self::Eq(col, v) => (format!("{col} = ?"), vec![v.clone()]),
270            Self::Ne(col, v) => (format!("{col} != ?"), vec![v.clone()]),
271            Self::Gt(col, v) => (format!("{col} > ?"), vec![v.clone()]),
272            Self::Gte(col, v) => (format!("{col} >= ?"), vec![v.clone()]),
273            Self::Lt(col, v) => (format!("{col} < ?"), vec![v.clone()]),
274            Self::Lte(col, v) => (format!("{col} <= ?"), vec![v.clone()]),
275            Self::Like(col, pat) => (format!("{col} LIKE ?"), vec![SqlValue::Text(pat.clone())]),
276            Self::NotLike(col, pat) => (
277                format!("{col} NOT LIKE ?"),
278                vec![SqlValue::Text(pat.clone())],
279            ),
280            Self::ILike(col, pat) => (format!("{col} ILIKE ?"), vec![SqlValue::Text(pat.clone())]),
281            Self::IsNull(col) => (format!("{col} IS NULL"), vec![]),
282            Self::IsNotNull(col) => (format!("{col} IS NOT NULL"), vec![]),
283            Self::In(col, vals) => {
284                let ph = vals.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
285                (format!("{col} IN ({ph})"), vals.clone())
286            }
287            Self::NotIn(col, vals) => {
288                let ph = vals.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
289                (format!("{col} NOT IN ({ph})"), vals.clone())
290            }
291            Self::Between(col, lo, hi) => (
292                format!("{col} BETWEEN ? AND ?"),
293                vec![lo.clone(), hi.clone()],
294            ),
295            Self::NotBetween(col, lo, hi) => (
296                format!("{col} NOT BETWEEN ? AND ?"),
297                vec![lo.clone(), hi.clone()],
298            ),
299            Self::Raw(sql) => (sql.clone(), vec![]),
300            Self::Group(inner) => {
301                let mut parts: Vec<String> = Vec::new();
302                let mut group_params: Vec<SqlValue> = Vec::new();
303                for (idx, (op, cond)) in inner.iter().enumerate() {
304                    let (frag, ps) = cond.to_param_sql_sqlite();
305                    group_params.extend(ps);
306                    if idx > 0 {
307                        parts.push(format!("{op} {frag}"));
308                    } else {
309                        parts.push(frag);
310                    }
311                }
312                (format!("({})", parts.join(" ")), group_params)
313            }
314            Self::JsonGet(col, key, _val) => (
315                format!("json_extract({col}, '$.{}') = ?", key.replace('\'', "''")),
316                vec![_val.clone()],
317            ),
318            Self::Subquery {
319                exists,
320                table,
321                fk_expr,
322                inner,
323            } => {
324                let kw = if *exists { "EXISTS" } else { "NOT EXISTS" };
325                let mut sub_params: Vec<SqlValue> = Vec::new();
326                let mut parts: Vec<String> = Vec::new();
327                for (i, (op, cond)) in inner.iter().enumerate() {
328                    let (frag, ps) = cond.to_param_sql_sqlite();
329                    sub_params.extend(ps);
330                    parts.push(if i == 0 {
331                        format!("AND {frag}")
332                    } else {
333                        format!("{op} {frag}")
334                    });
335                }
336                let extra = if parts.is_empty() {
337                    String::new()
338                } else {
339                    format!(" {}", parts.join(" "))
340                };
341                (
342                    format!("{kw} (SELECT 1 FROM {table} WHERE {fk_expr}{extra})"),
343                    sub_params,
344                )
345            }
346        }
347    }
348
349    /// Render as a SQL literal fragment (no parameterization — for debug output).
350    pub fn to_literal_sql(&self) -> String {
351        match self {
352            Self::Eq(col, v) => format!("{col} = {v}"),
353            Self::Ne(col, v) => format!("{col} != {v}"),
354            Self::Gt(col, v) => format!("{col} > {v}"),
355            Self::Gte(col, v) => format!("{col} >= {v}"),
356            Self::Lt(col, v) => format!("{col} < {v}"),
357            Self::Lte(col, v) => format!("{col} <= {v}"),
358            Self::Like(col, p) => format!("{col} LIKE '{p}'"),
359            Self::NotLike(col, p) => format!("{col} NOT LIKE '{p}'"),
360            Self::ILike(col, p) => format!("{col} ILIKE '{p}'"),
361            Self::IsNull(col) => format!("{col} IS NULL"),
362            Self::IsNotNull(col) => format!("{col} IS NOT NULL"),
363            Self::In(col, vals) => {
364                let lits: Vec<String> = vals.iter().map(|v| v.to_sql_literal()).collect();
365                format!("{col} IN ({})", lits.join(", "))
366            }
367            Self::NotIn(col, vals) => {
368                let lits: Vec<String> = vals.iter().map(|v| v.to_sql_literal()).collect();
369                format!("{col} NOT IN ({})", lits.join(", "))
370            }
371            Self::Between(col, lo, hi) => format!("{col} BETWEEN {lo} AND {hi}"),
372            Self::NotBetween(col, lo, hi) => format!("{col} NOT BETWEEN {lo} AND {hi}"),
373            Self::Raw(sql) => sql.clone(),
374            Self::Group(inner) => {
375                let parts: Vec<String> = inner
376                    .iter()
377                    .enumerate()
378                    .map(|(idx, (op, cond))| {
379                        let frag = cond.to_literal_sql();
380                        if idx > 0 {
381                            format!("{op} {frag}")
382                        } else {
383                            frag
384                        }
385                    })
386                    .collect();
387                format!("({})", parts.join(" "))
388            }
389            Self::JsonGet(col, key, val) => {
390                format!("{col}->>'{}' = {val}", key.replace('\'', "''"))
391            }
392            Self::Subquery {
393                exists,
394                table,
395                fk_expr,
396                inner,
397            } => {
398                let kw = if *exists { "EXISTS" } else { "NOT EXISTS" };
399                let parts: Vec<String> = inner
400                    .iter()
401                    .enumerate()
402                    .map(|(i, (op, cond))| {
403                        let frag = cond.to_literal_sql();
404                        if i == 0 {
405                            format!("AND {frag}")
406                        } else {
407                            format!("{op} {frag}")
408                        }
409                    })
410                    .collect();
411                let extra = if parts.is_empty() {
412                    String::new()
413                } else {
414                    format!(" {}", parts.join(" "))
415                };
416                format!("{kw} (SELECT 1 FROM {table} WHERE {fk_expr}{extra})")
417            }
418        }
419    }
420}
421
422// ── OrderDir ─────────────────────────────────────────────────────────────────
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub enum OrderDir {
426    Asc,
427    Desc,
428}
429
430impl fmt::Display for OrderDir {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        match self {
433            Self::Asc => write!(f, "ASC"),
434            Self::Desc => write!(f, "DESC"),
435        }
436    }
437}