Skip to main content

sqlmodel_schema/ddl/
mysql.rs

1//! MySQL DDL generator.
2//!
3//! MySQL has comprehensive ALTER TABLE support for most schema operations.
4
5use 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
13/// DDL generator for MySQL.
14pub 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            // Tables
26            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            // Columns
37            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                // MySQL uses MODIFY COLUMN for type changes
54                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                // MySQL uses MODIFY COLUMN - need to restate the entire column definition
68                // This is a simplified version; full impl would need to preserve other properties
69                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                // MySQL supports ALTER COLUMN ... SET DEFAULT / DROP DEFAULT
90                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            // Primary Keys
110            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            // Foreign Keys
129            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            // Unique Constraints
150            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                // MySQL drops unique constraints via DROP INDEX
169                vec![format!(
170                    "ALTER TABLE {} DROP INDEX {}",
171                    quote_identifier(table, Dialect::Mysql),
172                    quote_identifier(name, Dialect::Mysql)
173                )]
174            }
175
176            // Indexes
177            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// ============================================================================
194// Unit Tests
195// ============================================================================
196
197#[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}