1use super::{
6 DdlGenerator, format_fk_constraint, generate_add_column, generate_create_index,
7 generate_create_table, generate_drop_index, generate_drop_table, generate_rename_column,
8 generate_rename_table, quote_identifier,
9};
10use crate::diff::SchemaOperation;
11use crate::introspect::Dialect;
12
13pub struct MysqlDdlGenerator;
15
16impl DdlGenerator for MysqlDdlGenerator {
17 fn dialect(&self) -> &'static str {
18 "mysql"
19 }
20
21 fn generate(&self, op: &SchemaOperation) -> Vec<String> {
22 tracing::debug!(dialect = "mysql", op = ?op, "Generating DDL");
23
24 let statements = match op {
25 SchemaOperation::CreateTable(table) => {
27 vec![generate_create_table(table, Dialect::Mysql)]
28 }
29 SchemaOperation::DropTable(name) => {
30 vec![generate_drop_table(name, Dialect::Mysql)]
31 }
32 SchemaOperation::RenameTable { from, to } => {
33 vec![generate_rename_table(from, to, Dialect::Mysql)]
34 }
35
36 SchemaOperation::AddColumn { table, column } => {
38 vec![generate_add_column(table, column, Dialect::Mysql)]
39 }
40 SchemaOperation::DropColumn { table, column } => {
41 vec![format!(
42 "ALTER TABLE {} DROP COLUMN {}",
43 quote_identifier(table, Dialect::Mysql),
44 quote_identifier(column, Dialect::Mysql)
45 )]
46 }
47 SchemaOperation::AlterColumnType {
48 table,
49 column,
50 to_type,
51 ..
52 } => {
53 vec![format!(
55 "ALTER TABLE {} MODIFY COLUMN {} {}",
56 quote_identifier(table, Dialect::Mysql),
57 quote_identifier(column, Dialect::Mysql),
58 to_type
59 )]
60 }
61 SchemaOperation::AlterColumnNullable {
62 table,
63 column,
64 to_nullable,
65 ..
66 } => {
67 let null_str = if *to_nullable { "NULL" } else { "NOT NULL" };
70 tracing::warn!(
71 table = %table,
72 column = %column,
73 "MySQL ALTER COLUMN nullability requires full column definition - using simplified form"
74 );
75 vec![format!(
76 "-- MySQL: Use MODIFY COLUMN with full column definition\n\
77 ALTER TABLE {} MODIFY COLUMN {} {}",
78 quote_identifier(table, Dialect::Mysql),
79 quote_identifier(column, Dialect::Mysql),
80 null_str
81 )]
82 }
83 SchemaOperation::AlterColumnDefault {
84 table,
85 column,
86 to_default,
87 ..
88 } => {
89 if let Some(default) = to_default {
91 vec![format!(
92 "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {}",
93 quote_identifier(table, Dialect::Mysql),
94 quote_identifier(column, Dialect::Mysql),
95 default
96 )]
97 } else {
98 vec![format!(
99 "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT",
100 quote_identifier(table, Dialect::Mysql),
101 quote_identifier(column, Dialect::Mysql)
102 )]
103 }
104 }
105 SchemaOperation::RenameColumn { table, from, to } => {
106 vec![generate_rename_column(table, from, to, Dialect::Mysql)]
107 }
108
109 SchemaOperation::AddPrimaryKey { table, columns } => {
111 let cols: Vec<String> = columns
112 .iter()
113 .map(|c| quote_identifier(c, Dialect::Mysql))
114 .collect();
115 vec![format!(
116 "ALTER TABLE {} ADD PRIMARY KEY ({})",
117 quote_identifier(table, Dialect::Mysql),
118 cols.join(", ")
119 )]
120 }
121 SchemaOperation::DropPrimaryKey { table } => {
122 vec![format!(
123 "ALTER TABLE {} DROP PRIMARY KEY",
124 quote_identifier(table, Dialect::Mysql)
125 )]
126 }
127
128 SchemaOperation::AddForeignKey { table, fk } => {
130 let constraint_name = fk
131 .name
132 .clone()
133 .unwrap_or_else(|| format!("fk_{}_{}", table, fk.column));
134 vec![format!(
135 "ALTER TABLE {} ADD CONSTRAINT {} {}",
136 quote_identifier(table, Dialect::Mysql),
137 quote_identifier(&constraint_name, Dialect::Mysql),
138 format_fk_constraint(fk, Dialect::Mysql)
139 )]
140 }
141 SchemaOperation::DropForeignKey { table, name } => {
142 vec![format!(
143 "ALTER TABLE {} DROP FOREIGN KEY {}",
144 quote_identifier(table, Dialect::Mysql),
145 quote_identifier(name, Dialect::Mysql)
146 )]
147 }
148
149 SchemaOperation::AddUnique { table, constraint } => {
151 let cols: Vec<String> = constraint
152 .columns
153 .iter()
154 .map(|c| quote_identifier(c, Dialect::Mysql))
155 .collect();
156 let name = constraint
157 .name
158 .clone()
159 .unwrap_or_else(|| format!("uk_{}_{}", table, constraint.columns.join("_")));
160 vec![format!(
161 "ALTER TABLE {} ADD CONSTRAINT {} UNIQUE ({})",
162 quote_identifier(table, Dialect::Mysql),
163 quote_identifier(&name, Dialect::Mysql),
164 cols.join(", ")
165 )]
166 }
167 SchemaOperation::DropUnique { table, name } => {
168 vec![format!(
170 "ALTER TABLE {} DROP INDEX {}",
171 quote_identifier(table, Dialect::Mysql),
172 quote_identifier(name, Dialect::Mysql)
173 )]
174 }
175
176 SchemaOperation::CreateIndex { table, index } => {
178 vec![generate_create_index(table, index, Dialect::Mysql)]
179 }
180 SchemaOperation::DropIndex { table, name } => {
181 vec![generate_drop_index(table, name, Dialect::Mysql)]
182 }
183 };
184
185 for stmt in &statements {
186 tracing::trace!(sql = %stmt, "Generated MySQL DDL statement");
187 }
188
189 statements
190 }
191}
192
193#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::diff::SchemaOperation;
201 use crate::introspect::{
202 ColumnInfo, ForeignKeyInfo, IndexInfo, ParsedSqlType, TableInfo, UniqueConstraintInfo,
203 };
204
205 fn make_column(name: &str, sql_type: &str, nullable: bool) -> ColumnInfo {
206 ColumnInfo {
207 name: name.to_string(),
208 sql_type: sql_type.to_string(),
209 parsed_type: ParsedSqlType::parse(sql_type),
210 nullable,
211 default: None,
212 primary_key: false,
213 auto_increment: false,
214 comment: None,
215 }
216 }
217
218 fn make_table(name: &str, columns: Vec<ColumnInfo>, pk: Vec<&str>) -> TableInfo {
219 TableInfo {
220 name: name.to_string(),
221 columns,
222 primary_key: pk.into_iter().map(String::from).collect(),
223 foreign_keys: Vec::new(),
224 unique_constraints: Vec::new(),
225 check_constraints: Vec::new(),
226 indexes: Vec::new(),
227 comment: None,
228 }
229 }
230
231 #[test]
232 fn test_create_table() {
233 let ddl = MysqlDdlGenerator;
234 let table = make_table(
235 "heroes",
236 vec![
237 make_column("id", "INT", false),
238 make_column("name", "VARCHAR(100)", false),
239 ],
240 vec!["id"],
241 );
242 let op = SchemaOperation::CreateTable(table);
243 let stmts = ddl.generate(&op);
244
245 assert_eq!(stmts.len(), 1);
246 assert!(stmts[0].contains("CREATE TABLE IF NOT EXISTS"));
247 assert!(stmts[0].contains("`heroes`"));
248 }
249
250 #[test]
251 fn test_drop_table() {
252 let ddl = MysqlDdlGenerator;
253 let op = SchemaOperation::DropTable("heroes".to_string());
254 let stmts = ddl.generate(&op);
255
256 assert_eq!(stmts.len(), 1);
257 assert_eq!(stmts[0], "DROP TABLE IF EXISTS `heroes`");
258 }
259
260 #[test]
261 fn test_rename_table() {
262 let ddl = MysqlDdlGenerator;
263 let op = SchemaOperation::RenameTable {
264 from: "old_heroes".to_string(),
265 to: "heroes".to_string(),
266 };
267 let stmts = ddl.generate(&op);
268
269 assert_eq!(stmts.len(), 1);
270 assert!(stmts[0].contains("RENAME TABLE"));
271 }
272
273 #[test]
274 fn test_add_column() {
275 let ddl = MysqlDdlGenerator;
276 let op = SchemaOperation::AddColumn {
277 table: "heroes".to_string(),
278 column: make_column("age", "INT", true),
279 };
280 let stmts = ddl.generate(&op);
281
282 assert_eq!(stmts.len(), 1);
283 assert!(stmts[0].contains("ALTER TABLE"));
284 assert!(stmts[0].contains("ADD COLUMN"));
285 }
286
287 #[test]
288 fn test_drop_column() {
289 let ddl = MysqlDdlGenerator;
290 let op = SchemaOperation::DropColumn {
291 table: "heroes".to_string(),
292 column: "old_field".to_string(),
293 };
294 let stmts = ddl.generate(&op);
295
296 assert_eq!(stmts.len(), 1);
297 assert!(stmts[0].contains("ALTER TABLE"));
298 assert!(stmts[0].contains("DROP COLUMN"));
299 }
300
301 #[test]
302 fn test_alter_column_type() {
303 let ddl = MysqlDdlGenerator;
304 let op = SchemaOperation::AlterColumnType {
305 table: "heroes".to_string(),
306 column: "age".to_string(),
307 from_type: "INT".to_string(),
308 to_type: "BIGINT".to_string(),
309 };
310 let stmts = ddl.generate(&op);
311
312 assert_eq!(stmts.len(), 1);
313 assert!(stmts[0].contains("MODIFY COLUMN"));
314 assert!(stmts[0].contains("BIGINT"));
315 }
316
317 #[test]
318 fn test_alter_column_default_set() {
319 let ddl = MysqlDdlGenerator;
320 let op = SchemaOperation::AlterColumnDefault {
321 table: "heroes".to_string(),
322 column: "status".to_string(),
323 from_default: None,
324 to_default: Some("'active'".to_string()),
325 };
326 let stmts = ddl.generate(&op);
327
328 assert_eq!(stmts.len(), 1);
329 assert!(stmts[0].contains("SET DEFAULT"));
330 assert!(stmts[0].contains("'active'"));
331 }
332
333 #[test]
334 fn test_alter_column_default_drop() {
335 let ddl = MysqlDdlGenerator;
336 let op = SchemaOperation::AlterColumnDefault {
337 table: "heroes".to_string(),
338 column: "status".to_string(),
339 from_default: Some("'active'".to_string()),
340 to_default: None,
341 };
342 let stmts = ddl.generate(&op);
343
344 assert_eq!(stmts.len(), 1);
345 assert!(stmts[0].contains("DROP DEFAULT"));
346 }
347
348 #[test]
349 fn test_rename_column() {
350 let ddl = MysqlDdlGenerator;
351 let op = SchemaOperation::RenameColumn {
352 table: "heroes".to_string(),
353 from: "old_name".to_string(),
354 to: "name".to_string(),
355 };
356 let stmts = ddl.generate(&op);
357
358 assert_eq!(stmts.len(), 1);
359 assert!(stmts[0].contains("RENAME COLUMN"));
360 }
361
362 #[test]
363 fn test_add_primary_key() {
364 let ddl = MysqlDdlGenerator;
365 let op = SchemaOperation::AddPrimaryKey {
366 table: "heroes".to_string(),
367 columns: vec!["id".to_string()],
368 };
369 let stmts = ddl.generate(&op);
370
371 assert_eq!(stmts.len(), 1);
372 assert!(stmts[0].contains("ADD PRIMARY KEY"));
373 }
374
375 #[test]
376 fn test_drop_primary_key() {
377 let ddl = MysqlDdlGenerator;
378 let op = SchemaOperation::DropPrimaryKey {
379 table: "heroes".to_string(),
380 };
381 let stmts = ddl.generate(&op);
382
383 assert_eq!(stmts.len(), 1);
384 assert!(stmts[0].contains("DROP PRIMARY KEY"));
385 }
386
387 #[test]
388 fn test_add_foreign_key() {
389 let ddl = MysqlDdlGenerator;
390 let op = SchemaOperation::AddForeignKey {
391 table: "heroes".to_string(),
392 fk: ForeignKeyInfo {
393 name: Some("fk_heroes_team".to_string()),
394 column: "team_id".to_string(),
395 foreign_table: "teams".to_string(),
396 foreign_column: "id".to_string(),
397 on_delete: Some("CASCADE".to_string()),
398 on_update: None,
399 },
400 };
401 let stmts = ddl.generate(&op);
402
403 assert_eq!(stmts.len(), 1);
404 assert!(stmts[0].contains("ADD CONSTRAINT"));
405 assert!(stmts[0].contains("FOREIGN KEY"));
406 assert!(stmts[0].contains("ON DELETE CASCADE"));
407 }
408
409 #[test]
410 fn test_drop_foreign_key() {
411 let ddl = MysqlDdlGenerator;
412 let op = SchemaOperation::DropForeignKey {
413 table: "heroes".to_string(),
414 name: "fk_heroes_team".to_string(),
415 };
416 let stmts = ddl.generate(&op);
417
418 assert_eq!(stmts.len(), 1);
419 assert!(stmts[0].contains("DROP FOREIGN KEY"));
420 }
421
422 #[test]
423 fn test_add_unique() {
424 let ddl = MysqlDdlGenerator;
425 let op = SchemaOperation::AddUnique {
426 table: "heroes".to_string(),
427 constraint: UniqueConstraintInfo {
428 name: Some("uk_heroes_name".to_string()),
429 columns: vec!["name".to_string()],
430 },
431 };
432 let stmts = ddl.generate(&op);
433
434 assert_eq!(stmts.len(), 1);
435 assert!(stmts[0].contains("ADD CONSTRAINT"));
436 assert!(stmts[0].contains("UNIQUE"));
437 }
438
439 #[test]
440 fn test_drop_unique() {
441 let ddl = MysqlDdlGenerator;
442 let op = SchemaOperation::DropUnique {
443 table: "heroes".to_string(),
444 name: "uk_heroes_name".to_string(),
445 };
446 let stmts = ddl.generate(&op);
447
448 assert_eq!(stmts.len(), 1);
449 assert!(stmts[0].contains("DROP INDEX"));
450 }
451
452 #[test]
453 fn test_create_index() {
454 let ddl = MysqlDdlGenerator;
455 let op = SchemaOperation::CreateIndex {
456 table: "heroes".to_string(),
457 index: IndexInfo {
458 name: "idx_heroes_name".to_string(),
459 columns: vec!["name".to_string()],
460 unique: false,
461 index_type: None,
462 primary: false,
463 },
464 };
465 let stmts = ddl.generate(&op);
466
467 assert_eq!(stmts.len(), 1);
468 assert!(stmts[0].contains("CREATE INDEX"));
469 }
470
471 #[test]
472 fn test_drop_index() {
473 let ddl = MysqlDdlGenerator;
474 let op = SchemaOperation::DropIndex {
475 table: "heroes".to_string(),
476 name: "idx_heroes_name".to_string(),
477 };
478 let stmts = ddl.generate(&op);
479
480 assert_eq!(stmts.len(), 1);
481 assert!(stmts[0].contains("DROP INDEX"));
482 assert!(stmts[0].contains("ON `heroes`"));
483 }
484
485 #[test]
486 fn test_dialect() {
487 let ddl = MysqlDdlGenerator;
488 assert_eq!(ddl.dialect(), "mysql");
489 }
490}