1use std::borrow::Cow;
2
3use keelson_core::expr::{Chain, Expr, IntoExpr, IntoExprList};
4
5#[allow(clippy::wrong_self_convention)]
31pub trait MysqlOps: Chain {
32 #[must_use]
36 fn not_like(self, rhs: impl IntoExpr) -> Self {
37 self.op("NOT LIKE", rhs)
38 }
39
40 #[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 #[must_use]
50 fn regexp(self, rhs: impl IntoExpr) -> Self {
51 self.op("REGEXP", rhs)
52 }
53
54 #[must_use]
56 fn not_regexp(self, rhs: impl IntoExpr) -> Self {
57 self.op("NOT REGEXP", rhs)
58 }
59
60 #[must_use]
62 fn rlike(self, rhs: impl IntoExpr) -> Self {
63 self.op("RLIKE", rhs)
64 }
65
66 #[must_use]
68 fn sounds_like(self, rhs: impl IntoExpr) -> Self {
69 self.op("SOUNDS LIKE", rhs)
70 }
71
72 #[must_use]
77 fn null_safe_eq(self, rhs: impl IntoExpr) -> Self {
78 self.op("<=>", rhs)
79 }
80
81 #[must_use]
84 fn bang_eq(self, rhs: impl IntoExpr) -> Self {
85 self.op("!=", rhs)
86 }
87
88 #[must_use]
92 fn xor(self, rhs: impl IntoExpr) -> Self {
93 self.op("XOR", rhs)
94 }
95
96 #[must_use]
98 fn is_true(self) -> Self {
99 self.step(|lhs| Expr::postfix(lhs, "IS TRUE"))
100 }
101
102 #[must_use]
104 fn is_not_true(self) -> Self {
105 self.step(|lhs| Expr::postfix(lhs, "IS NOT TRUE"))
106 }
107
108 #[must_use]
110 fn is_false(self) -> Self {
111 self.step(|lhs| Expr::postfix(lhs, "IS FALSE"))
112 }
113
114 #[must_use]
116 fn is_not_false(self) -> Self {
117 self.step(|lhs| Expr::postfix(lhs, "IS NOT FALSE"))
118 }
119
120 #[must_use]
122 fn is_unknown(self) -> Self {
123 self.step(|lhs| Expr::postfix(lhs, "IS UNKNOWN"))
124 }
125
126 #[must_use]
128 fn is_not_unknown(self) -> Self {
129 self.step(|lhs| Expr::postfix(lhs, "IS NOT UNKNOWN"))
130 }
131
132 #[must_use]
136 fn times(self, rhs: impl IntoExpr) -> Self {
137 self.op("*", rhs)
138 }
139
140 #[must_use]
142 fn divide(self, rhs: impl IntoExpr) -> Self {
143 self.op("/", rhs)
144 }
145
146 #[must_use]
148 fn div(self, rhs: impl IntoExpr) -> Self {
149 self.op("DIV", rhs)
150 }
151
152 #[must_use]
154 fn modulo(self, rhs: impl IntoExpr) -> Self {
155 self.op("MOD", rhs)
156 }
157
158 #[must_use]
160 fn bit_and(self, rhs: impl IntoExpr) -> Self {
161 self.op("&", rhs)
162 }
163
164 #[must_use]
166 fn bit_or(self, rhs: impl IntoExpr) -> Self {
167 self.op("|", rhs)
168 }
169
170 #[must_use]
173 fn bit_xor(self, rhs: impl IntoExpr) -> Self {
174 self.op("^", rhs)
175 }
176
177 #[must_use]
179 fn shift_left(self, rhs: impl IntoExpr) -> Self {
180 self.op("<<", rhs)
181 }
182
183 #[must_use]
185 fn shift_right(self, rhs: impl IntoExpr) -> Self {
186 self.op(">>", rhs)
187 }
188
189 #[must_use]
193 fn json_get(self, path: impl IntoExpr) -> Self {
194 self.op("->", path)
195 }
196
197 #[must_use]
199 fn json_get_text(self, path: impl IntoExpr) -> Self {
200 self.op("->>", path)
201 }
202
203 #[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 #[must_use]
214 fn eq_any(self, subquery: impl IntoExprList) -> Self {
215 self.any("=", subquery)
216 }
217
218 #[must_use]
220 fn ne_all(self, subquery: impl IntoExprList) -> Self {
221 self.all("<>", subquery)
222 }
223
224 #[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 #[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 #[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 #[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 #[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 #[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}