1use crate::{Dialect, Sql};
4use nautilus_core::{BinaryOp, Delete, Expr, Insert, Result, Select, Update, Value};
5
6#[derive(Debug, Clone, Copy)]
8pub struct MysqlDialect;
9
10impl Dialect for MysqlDialect {
13 fn supports_returning(&self) -> bool {
14 false
15 }
16
17 fn render_select(&self, select: &Select) -> Result<Sql> {
18 let mut ctx = RenderContext::new();
19 render_select_body_core!(&mut ctx, select, quote_identifier, render_expr, false, true);
21 Ok(Sql {
22 text: ctx.sql,
23 params: ctx.params,
24 })
25 }
26
27 fn render_insert(&self, insert: &Insert) -> Result<Sql> {
28 let mut ctx = RenderContext::new();
29 render_insert_body!(&mut ctx, insert, quote_identifier, false, false);
30 Ok(Sql {
31 text: ctx.sql,
32 params: ctx.params,
33 })
34 }
35
36 fn render_update(&self, update: &Update) -> Result<Sql> {
37 let mut ctx = RenderContext::new();
38 render_update_body!(
39 &mut ctx,
40 update,
41 quote_identifier,
42 render_expr,
43 false,
44 false
45 );
46 Ok(Sql {
47 text: ctx.sql,
48 params: ctx.params,
49 })
50 }
51
52 fn render_delete(&self, delete: &Delete) -> Result<Sql> {
53 let mut ctx = RenderContext::new();
54 render_delete_body!(&mut ctx, delete, quote_identifier, render_expr, false);
55 Ok(Sql {
56 text: ctx.sql,
57 params: ctx.params,
58 })
59 }
60}
61
62fn quote_identifier(name: &str) -> String {
64 crate::backtick_quote_identifier(name)
65}
66
67struct RenderContext {
69 sql: String,
70 params: Vec<Value>,
71}
72
73impl RenderContext {
74 fn new() -> Self {
75 Self {
76 sql: String::new(),
77 params: Vec::new(),
78 }
79 }
80
81 fn push_param(&mut self, value: Value) -> String {
83 self.params.push(value);
84 "?".to_string()
85 }
86}
87
88fn render_select_body(ctx: &mut RenderContext, select: &crate::Select) {
92 render_select_body_core!(ctx, select, quote_identifier, render_expr, false, true);
94}
95
96fn render_expr(ctx: &mut RenderContext, expr: &Expr) {
98 render_expr_common!(ctx, expr, quote_identifier, render_expr, render_select_body, {
99 Expr::Param(value) => {
100 if matches!(value, Value::Null) {
102 ctx.sql.push_str("NULL");
103 } else {
104 let placeholder = ctx.push_param(value.clone());
105 ctx.sql.push_str(&placeholder);
106 }
107 }
108 Expr::Binary { left, op, right } => {
109 if matches!(op, BinaryOp::In | BinaryOp::NotIn) {
110 ctx.sql.push('(');
111 render_expr(ctx, left);
112 ctx.sql.push(' ');
113 ctx.sql.push_str(if matches!(op, BinaryOp::In) { "IN" } else { "NOT IN" });
114 ctx.sql.push_str(" (");
115 if let Expr::List(exprs) = right.as_ref() {
116 for (i, e) in exprs.iter().enumerate() {
117 if i > 0 { ctx.sql.push_str(", "); }
118 render_expr(ctx, e);
119 }
120 } else {
121 render_expr(ctx, right);
122 }
123 ctx.sql.push(')');
124 ctx.sql.push(')');
125 } else if matches!(op, BinaryOp::ArrayContains | BinaryOp::ArrayContainedBy | BinaryOp::ArrayOverlaps) {
126 match op {
129 BinaryOp::ArrayContains => {
130 ctx.sql.push_str("JSON_CONTAINS(");
133 render_expr(ctx, left);
134 ctx.sql.push_str(", ");
135 render_expr(ctx, right);
136 ctx.sql.push(')');
137 }
138 BinaryOp::ArrayContainedBy => {
139 ctx.sql.push_str("JSON_CONTAINS(");
141 render_expr(ctx, right);
142 ctx.sql.push_str(", ");
143 render_expr(ctx, left);
144 ctx.sql.push(')');
145 }
146 BinaryOp::ArrayOverlaps => {
147 ctx.sql.push_str("JSON_OVERLAPS(");
149 render_expr(ctx, left);
150 ctx.sql.push_str(", ");
151 render_expr(ctx, right);
152 ctx.sql.push(')');
153 }
154 _ => unreachable!(),
155 }
156 } else {
157 ctx.sql.push('(');
158 render_expr(ctx, left);
159 ctx.sql.push(' ');
160 ctx.sql.push_str(crate::binary_op_sql(op));
161 ctx.sql.push(' ');
162 render_expr(ctx, right);
163 ctx.sql.push(')');
164 }
165 }
166 Expr::FunctionCall { name, args } => {
167 let mysql_name = match name.as_str() {
169 "json_agg" => "JSON_ARRAYAGG",
170 "json_build_object" => "JSON_OBJECT",
171 _ => name,
172 };
173 ctx.sql.push_str(mysql_name);
174 ctx.sql.push('(');
175 for (i, arg) in args.iter().enumerate() {
176 if i > 0 { ctx.sql.push_str(", "); }
177 render_expr(ctx, arg);
178 }
179 ctx.sql.push(')');
180 }
181 Expr::Filter { expr, predicate } => {
182 if let Expr::FunctionCall { name, args } = expr.as_ref() {
184 let mysql_name = match name.as_str() {
185 "json_agg" => "JSON_ARRAYAGG",
186 "json_build_object" => "JSON_OBJECT",
187 _ => name,
188 };
189 ctx.sql.push_str(mysql_name);
190 ctx.sql.push_str("(CASE WHEN ");
191 render_expr(ctx, predicate);
192 ctx.sql.push_str(" THEN ");
193 if !args.is_empty() {
194 render_expr(ctx, &args[0]);
195 } else {
196 ctx.sql.push_str("NULL");
197 }
198 ctx.sql.push_str(" ELSE NULL END)");
199 } else {
200 render_expr(ctx, expr);
202 }
203 }
204 });
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_quote_identifier() {
213 assert_eq!(quote_identifier("users"), "`users`");
214 assert_eq!(quote_identifier("email"), "`email`");
215 assert_eq!(quote_identifier("foo`bar"), "`foo``bar`");
216 assert_eq!(quote_identifier("a`b`c"), "`a``b``c`");
217 }
218
219 #[test]
222 fn test_skip_without_take() {
223 let dialect = MysqlDialect;
224 let select = Select::from_table("users").skip(20).build().unwrap();
225 let sql = dialect.render_select(&select).unwrap();
226
227 assert_eq!(
229 sql.text,
230 "SELECT * FROM `users` LIMIT 18446744073709551615 OFFSET 20"
231 );
232 assert!(sql.params.is_empty());
233 }
234
235 #[test]
238 fn test_insert_returning_is_omitted() {
239 let dialect = MysqlDialect;
240 let insert = Insert::into_table("users")
241 .column(nautilus_core::ColumnMarker::new("users", "email"))
242 .values(vec![Value::String("alice@example.com".to_string())])
243 .returning(vec![
244 nautilus_core::ColumnMarker::new("users", "id"),
245 nautilus_core::ColumnMarker::new("users", "email"),
246 ])
247 .build()
248 .unwrap();
249 let sql = dialect.render_insert(&insert).unwrap();
250
251 assert_eq!(sql.text, "INSERT INTO `users` (`email`) VALUES (?)");
252 assert!(!sql.text.contains("RETURNING"));
253 }
254
255 #[test]
256 fn test_update_returning_is_omitted() {
257 let dialect = MysqlDialect;
258 let update = Update::table("users")
259 .set(
260 nautilus_core::ColumnMarker::new("users", "email"),
261 Value::String("new@example.com".to_string()),
262 )
263 .filter(Expr::column("id").eq(Expr::param(Value::I64(1))))
264 .returning(vec![
265 nautilus_core::ColumnMarker::new("users", "id"),
266 nautilus_core::ColumnMarker::new("users", "email"),
267 ])
268 .build()
269 .unwrap();
270 let sql = dialect.render_update(&update).unwrap();
271
272 assert_eq!(sql.text, "UPDATE `users` SET `email` = ? WHERE (`id` = ?)");
273 assert!(!sql.text.contains("RETURNING"));
274 }
275
276 #[test]
277 fn test_delete_returning_is_omitted() {
278 let dialect = MysqlDialect;
279 let delete = Delete::from_table("users")
280 .filter(Expr::column("id").eq(Expr::param(Value::I64(1))))
281 .returning(vec![
282 nautilus_core::ColumnMarker::new("users", "id"),
283 nautilus_core::ColumnMarker::new("users", "email"),
284 ])
285 .build()
286 .unwrap();
287 let sql = dialect.render_delete(&delete).unwrap();
288
289 assert_eq!(sql.text, "DELETE FROM `users` WHERE (`id` = ?)");
290 assert!(!sql.text.contains("RETURNING"));
291 }
292}