Skip to main content

keelson_mysql/
ops.rs

1use std::borrow::Cow;
2
3use keelson_core::expr::{Chain, Expr, IntoExpr, IntoExprList};
4
5/// The operators MySQL has and the other two dialects do not.
6///
7/// An extension trait over [`Chain`] with a blanket impl, which is the shape
8/// `keelson_core::expr::chain` documents for exactly this: nothing in core changes,
9/// and the operators are reachable only where this trait is imported.
10///
11/// ```
12/// use keelson_mysql::{MysqlOps, arg, quote};
13///
14/// let e = quote("name").regexp(arg("^a"));
15/// ```
16///
17/// Every method finishes through [`Chain::step`] or [`Chain::op`], so the
18/// parenthesisation rule is applied for it and a result never accumulates redundant
19/// parentheses.
20///
21/// # A caution about the shared operators
22///
23/// [`Chain`] carries `is_distinct_from` and `is_not_distinct_from` for every
24/// dialect, and **MySQL has neither**. Its null-safe comparison is
25/// [`null_safe_eq`](MysqlOps::null_safe_eq), the `<=>` operator; the SQL-standard
26/// spelling does not parse here. That one cannot be hidden, because the method lives
27/// on the shared trait.
28// The `is_*` predicates take `self` by value like every other operator: they are SQL
29// keywords being spelled out, not Rust predicates returning `bool`.
30#[allow(clippy::wrong_self_convention)]
31pub trait MysqlOps: Chain {
32    // -- pattern matching ----------------------------------------------------
33
34    /// `self NOT LIKE rhs`.
35    #[must_use]
36    fn not_like(self, rhs: impl IntoExpr) -> Self {
37        self.op("NOT LIKE", rhs)
38    }
39
40    /// `self LIKE pattern ESCAPE escape` — name the escape character.
41    #[must_use]
42    fn like_escape(self, pattern: impl IntoExpr, escape: impl IntoExpr) -> Self {
43        self.step(move |lhs| {
44            Expr::join((lhs, Expr::raw("LIKE"), pattern, Expr::raw("ESCAPE"), escape))
45        })
46    }
47
48    /// `self REGEXP rhs` — extended regular-expression match (*14.8.2*).
49    #[must_use]
50    fn regexp(self, rhs: impl IntoExpr) -> Self {
51        self.op("REGEXP", rhs)
52    }
53
54    /// `self NOT REGEXP rhs`.
55    #[must_use]
56    fn not_regexp(self, rhs: impl IntoExpr) -> Self {
57        self.op("NOT REGEXP", rhs)
58    }
59
60    /// `self RLIKE rhs`, MySQL's synonym for `REGEXP`.
61    #[must_use]
62    fn rlike(self, rhs: impl IntoExpr) -> Self {
63        self.op("RLIKE", rhs)
64    }
65
66    /// `self SOUNDS LIKE rhs` — equal under `SOUNDEX`.
67    #[must_use]
68    fn sounds_like(self, rhs: impl IntoExpr) -> Self {
69        self.op("SOUNDS LIKE", rhs)
70    }
71
72    // -- comparison ----------------------------------------------------------
73
74    /// `self <=> rhs` — the null-safe equality operator, which is what MySQL has
75    /// instead of `IS NOT DISTINCT FROM`.
76    #[must_use]
77    fn null_safe_eq(self, rhs: impl IntoExpr) -> Self {
78        self.op("<=>", rhs)
79    }
80
81    /// `self != rhs`. MySQL accepts both this and the standard `<>`, which is
82    /// [`Chain::ne`].
83    #[must_use]
84    fn bang_eq(self, rhs: impl IntoExpr) -> Self {
85        self.op("!=", rhs)
86    }
87
88    // -- logic ---------------------------------------------------------------
89
90    /// `self XOR rhs` — logical exclusive or.
91    #[must_use]
92    fn xor(self, rhs: impl IntoExpr) -> Self {
93        self.op("XOR", rhs)
94    }
95
96    /// `self IS TRUE`.
97    #[must_use]
98    fn is_true(self) -> Self {
99        self.step(|lhs| Expr::postfix(lhs, "IS TRUE"))
100    }
101
102    /// `self IS NOT TRUE`.
103    #[must_use]
104    fn is_not_true(self) -> Self {
105        self.step(|lhs| Expr::postfix(lhs, "IS NOT TRUE"))
106    }
107
108    /// `self IS FALSE`.
109    #[must_use]
110    fn is_false(self) -> Self {
111        self.step(|lhs| Expr::postfix(lhs, "IS FALSE"))
112    }
113
114    /// `self IS NOT FALSE`.
115    #[must_use]
116    fn is_not_false(self) -> Self {
117        self.step(|lhs| Expr::postfix(lhs, "IS NOT FALSE"))
118    }
119
120    /// `self IS UNKNOWN` — true when the operand is `NULL`.
121    #[must_use]
122    fn is_unknown(self) -> Self {
123        self.step(|lhs| Expr::postfix(lhs, "IS UNKNOWN"))
124    }
125
126    /// `self IS NOT UNKNOWN`.
127    #[must_use]
128    fn is_not_unknown(self) -> Self {
129        self.step(|lhs| Expr::postfix(lhs, "IS NOT UNKNOWN"))
130    }
131
132    // -- arithmetic and bits -------------------------------------------------
133
134    /// `self * rhs`.
135    #[must_use]
136    fn times(self, rhs: impl IntoExpr) -> Self {
137        self.op("*", rhs)
138    }
139
140    /// `self / rhs` — floating-point division.
141    #[must_use]
142    fn divide(self, rhs: impl IntoExpr) -> Self {
143        self.op("/", rhs)
144    }
145
146    /// `self DIV rhs` — integer division.
147    #[must_use]
148    fn div(self, rhs: impl IntoExpr) -> Self {
149        self.op("DIV", rhs)
150    }
151
152    /// `self MOD rhs`. `%` is the same operator; this is the keyword spelling.
153    #[must_use]
154    fn modulo(self, rhs: impl IntoExpr) -> Self {
155        self.op("MOD", rhs)
156    }
157
158    /// `self & rhs` — bitwise and.
159    #[must_use]
160    fn bit_and(self, rhs: impl IntoExpr) -> Self {
161        self.op("&", rhs)
162    }
163
164    /// `self | rhs` — bitwise or.
165    #[must_use]
166    fn bit_or(self, rhs: impl IntoExpr) -> Self {
167        self.op("|", rhs)
168    }
169
170    /// `self ^ rhs` — bitwise exclusive or. Not exponentiation, which MySQL spells
171    /// `POW`.
172    #[must_use]
173    fn bit_xor(self, rhs: impl IntoExpr) -> Self {
174        self.op("^", rhs)
175    }
176
177    /// `self << rhs` — left shift.
178    #[must_use]
179    fn shift_left(self, rhs: impl IntoExpr) -> Self {
180        self.op("<<", rhs)
181    }
182
183    /// `self >> rhs` — right shift.
184    #[must_use]
185    fn shift_right(self, rhs: impl IntoExpr) -> Self {
186        self.op(">>", rhs)
187    }
188
189    // -- JSON ----------------------------------------------------------------
190
191    /// `self -> path` — `JSON_EXTRACT`, keeping the JSON quoting.
192    #[must_use]
193    fn json_get(self, path: impl IntoExpr) -> Self {
194        self.op("->", path)
195    }
196
197    /// `self ->> path` — `JSON_UNQUOTE(JSON_EXTRACT(…))`.
198    #[must_use]
199    fn json_get_text(self, path: impl IntoExpr) -> Self {
200        self.op("->>", path)
201    }
202
203    /// `self MEMBER OF (array)` — whether this value is an element of a JSON array
204    /// (MySQL 8.0.17).
205    #[must_use]
206    fn member_of(self, array: impl IntoExpr) -> Self {
207        self.step(move |lhs| Expr::binary(lhs, "MEMBER OF", Expr::group(array.into_expr())))
208    }
209
210    // -- quantified comparison ------------------------------------------------
211
212    /// `self = ANY (subquery)`.
213    #[must_use]
214    fn eq_any(self, subquery: impl IntoExprList) -> Self {
215        self.any("=", subquery)
216    }
217
218    /// `self <> ALL (subquery)`.
219    #[must_use]
220    fn ne_all(self, subquery: impl IntoExprList) -> Self {
221        self.all("<>", subquery)
222    }
223
224    /// `self <op> ANY (subquery)` — true if the comparison holds for some row.
225    #[must_use]
226    fn any(self, op: &'static str, subquery: impl IntoExprList) -> Self {
227        self.step(move |lhs| {
228            Expr::join((
229                Expr::binary(lhs, op, Expr::raw("ANY")),
230                Expr::group(subquery),
231            ))
232        })
233    }
234
235    /// `self <op> ALL (subquery)` — true if the comparison holds for every row.
236    #[must_use]
237    fn all(self, op: &'static str, subquery: impl IntoExprList) -> Self {
238        self.step(move |lhs| {
239            Expr::join((
240                Expr::binary(lhs, op, Expr::raw("ALL")),
241                Expr::group(subquery),
242            ))
243        })
244    }
245
246    // -- decorations ---------------------------------------------------------
247
248    /// `self COLLATE \`name\`` — compare or sort under a named collation.
249    #[must_use]
250    fn collate(self, name: impl Into<Cow<'static, str>>) -> Self {
251        let name = name.into();
252        self.step(move |lhs| Expr::join((lhs, Expr::raw("COLLATE"), Expr::ident(name))))
253    }
254
255    /// `BINARY self` — compare as a binary string, which is how MySQL is made
256    /// case-sensitive without naming a collation.
257    #[must_use]
258    fn binary(self) -> Self {
259        self.step(|lhs| Expr::prefix("BINARY", lhs))
260    }
261}
262
263impl<C: Chain> MysqlOps for C {}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use crate::{Mysql, arg, quote, s};
269    use keelson_core::build;
270
271    fn sql(e: Expr) -> String {
272        build(&Mysql, &e).expect("render").0
273    }
274
275    /// One set of parentheses per operator, from the chain's own rule.
276    #[test]
277    fn every_operator_renders_with_one_set_of_parentheses() {
278        let col = || quote("name");
279        for (produced, expected) in [
280            (col().not_like(arg("a%")), "(`name` NOT LIKE ?)"),
281            (col().regexp(arg("^a")), "(`name` REGEXP ?)"),
282            (col().not_regexp(arg("^a")), "(`name` NOT REGEXP ?)"),
283            (col().rlike(arg("^a")), "(`name` RLIKE ?)"),
284            (col().sounds_like(arg("robert")), "(`name` SOUNDS LIKE ?)"),
285            (col().null_safe_eq(arg("a")), "(`name` <=> ?)"),
286            (col().bang_eq(arg("a")), "(`name` != ?)"),
287            (col().xor(arg(true)), "(`name` XOR ?)"),
288            (col().is_true(), "(`name` IS TRUE)"),
289            (col().is_not_true(), "(`name` IS NOT TRUE)"),
290            (col().is_false(), "(`name` IS FALSE)"),
291            (col().is_not_false(), "(`name` IS NOT FALSE)"),
292            (col().is_unknown(), "(`name` IS UNKNOWN)"),
293            (col().is_not_unknown(), "(`name` IS NOT UNKNOWN)"),
294            (col().times(2i32), "(`name` * 2)"),
295            (col().divide(2i32), "(`name` / 2)"),
296            (col().div(2i32), "(`name` DIV 2)"),
297            (col().modulo(2i32), "(`name` MOD 2)"),
298            (col().bit_and(3i32), "(`name` & 3)"),
299            (col().bit_or(3i32), "(`name` | 3)"),
300            (col().bit_xor(3i32), "(`name` ^ 3)"),
301            (col().shift_left(1i32), "(`name` << 1)"),
302            (col().shift_right(1i32), "(`name` >> 1)"),
303            (col().json_get(s("$.a")), "(`name` -> '$.a')"),
304            (col().json_get_text(s("$.a")), "(`name` ->> '$.a')"),
305            (col().binary(), "(BINARY `name`)"),
306        ] {
307            assert_eq!(sql(produced), expected);
308        }
309    }
310
311    /// The multi-token forms, where the shape is the thing worth pinning.
312    #[test]
313    fn the_multi_token_operators_keep_their_shape() {
314        assert_eq!(
315            sql(quote("name").like_escape(arg("a!_b"), s("!"))),
316            "(`name` LIKE ? ESCAPE '!')"
317        );
318        assert_eq!(
319            sql(arg(3i32).member_of(quote("body"))),
320            "(? MEMBER OF (`body`))"
321        );
322        assert_eq!(
323            sql(quote("id").eq_any(quote("sub"))),
324            "(`id` = ANY (`sub`))"
325        );
326        assert_eq!(
327            sql(quote("id").ne_all(quote("sub"))),
328            "(`id` <> ALL (`sub`))"
329        );
330        assert_eq!(
331            sql(quote("id").any(">", quote("sub"))),
332            "(`id` > ANY (`sub`))"
333        );
334        assert_eq!(
335            sql(quote("name").collate("utf8mb4_bin")),
336            "(`name` COLLATE `utf8mb4_bin`)"
337        );
338    }
339}