1mod mysql;
7mod postgres;
8mod sqlite;
9
10pub use mysql::MysqlDdlGenerator;
11pub use postgres::PostgresDdlGenerator;
12pub use sqlite::SqliteDdlGenerator;
13
14use crate::diff::SchemaOperation;
15use crate::introspect::{
16 ColumnInfo, Dialect, ForeignKeyInfo, IndexInfo, TableInfo, UniqueConstraintInfo,
17};
18
19pub trait DdlGenerator {
21 fn dialect(&self) -> &'static str;
23
24 fn generate(&self, op: &SchemaOperation) -> Vec<String>;
28
29 fn generate_all(&self, ops: &[SchemaOperation]) -> Vec<String> {
31 ops.iter().flat_map(|op| self.generate(op)).collect()
32 }
33
34 fn generate_rollback(&self, ops: &[SchemaOperation]) -> Vec<String> {
39 ops.iter()
40 .rev()
41 .filter_map(|op| op.inverse())
42 .flat_map(|op| self.generate(&op))
43 .collect()
44 }
45}
46
47pub fn generator_for_dialect(dialect: Dialect) -> Box<dyn DdlGenerator> {
49 match dialect {
50 Dialect::Sqlite => Box::new(SqliteDdlGenerator),
51 Dialect::Mysql => Box::new(MysqlDdlGenerator),
52 Dialect::Postgres => Box::new(PostgresDdlGenerator),
53 }
54}
55
56fn quote_identifier(name: &str, dialect: Dialect) -> String {
66 match dialect {
67 Dialect::Mysql => format!("`{}`", name.replace('`', "``")),
68 Dialect::Sqlite | Dialect::Postgres => format!("\"{}\"", name.replace('"', "\"\"")),
69 }
70}
71
72fn format_column_def(col: &ColumnInfo, dialect: Dialect) -> String {
74 let mut parts = vec![quote_identifier(&col.name, dialect), col.sql_type.clone()];
75
76 if !col.nullable {
77 parts.push("NOT NULL".to_string());
78 }
79
80 if let Some(ref default) = col.default {
81 parts.push(format!("DEFAULT {}", default));
82 }
83
84 match dialect {
86 Dialect::Sqlite => {
87 }
90 Dialect::Mysql => {
91 if col.auto_increment {
92 parts.push("AUTO_INCREMENT".to_string());
93 }
94 }
95 Dialect::Postgres => {
96 }
99 }
100
101 parts.join(" ")
102}
103
104fn format_referential_action(action: Option<&String>) -> &str {
106 match action.map(|s| s.to_uppercase()).as_deref() {
107 Some("CASCADE") => "CASCADE",
108 Some("SET NULL") => "SET NULL",
109 Some("SET DEFAULT") => "SET DEFAULT",
110 Some("RESTRICT") => "RESTRICT",
111 _ => "NO ACTION",
112 }
113}
114
115fn format_fk_constraint(fk: &ForeignKeyInfo, dialect: Dialect) -> String {
117 let mut sql = format!(
118 "FOREIGN KEY ({}) REFERENCES {}({})",
119 quote_identifier(&fk.column, dialect),
120 quote_identifier(&fk.foreign_table, dialect),
121 quote_identifier(&fk.foreign_column, dialect),
122 );
123
124 let on_delete = format_referential_action(fk.on_delete.as_ref());
125 let on_update = format_referential_action(fk.on_update.as_ref());
126
127 if on_delete != "NO ACTION" {
128 sql.push_str(&format!(" ON DELETE {}", on_delete));
129 }
130 if on_update != "NO ACTION" {
131 sql.push_str(&format!(" ON UPDATE {}", on_update));
132 }
133
134 sql
135}
136
137fn format_unique_constraint(unique: &UniqueConstraintInfo, dialect: Dialect) -> String {
139 let cols: Vec<String> = unique
140 .columns
141 .iter()
142 .map(|c| quote_identifier(c, dialect))
143 .collect();
144
145 if let Some(ref name) = unique.name {
146 format!(
147 "CONSTRAINT {} UNIQUE ({})",
148 quote_identifier(name, dialect),
149 cols.join(", ")
150 )
151 } else {
152 format!("UNIQUE ({})", cols.join(", "))
153 }
154}
155
156fn generate_create_table_with_if_not_exists(
161 table: &TableInfo,
162 dialect: Dialect,
163 if_not_exists: bool,
164) -> String {
165 tracing::debug!(
166 dialect = %match dialect {
167 Dialect::Sqlite => "sqlite",
168 Dialect::Mysql => "mysql",
169 Dialect::Postgres => "postgres",
170 },
171 table = %table.name,
172 columns = table.columns.len(),
173 "Generating CREATE TABLE DDL"
174 );
175
176 let mut parts = Vec::new();
177
178 for col in &table.columns {
180 parts.push(format!(" {}", format_column_def(col, dialect)));
181 }
182
183 if !table.primary_key.is_empty() {
185 let pk_cols: Vec<String> = table
186 .primary_key
187 .iter()
188 .map(|c| quote_identifier(c, dialect))
189 .collect();
190 parts.push(format!(" PRIMARY KEY ({})", pk_cols.join(", ")));
191 }
192
193 for unique in &table.unique_constraints {
195 parts.push(format!(" {}", format_unique_constraint(unique, dialect)));
196 }
197
198 for fk in &table.foreign_keys {
200 parts.push(format!(" {}", format_fk_constraint(fk, dialect)));
201 }
202
203 let table_name = quote_identifier(&table.name, dialect);
204 let ine = if if_not_exists { " IF NOT EXISTS" } else { "" };
205 let sql = format!(
206 "CREATE TABLE{} {} (\n{}\n)",
207 ine,
208 table_name,
209 parts.join(",\n")
210 );
211
212 tracing::trace!(sql = %sql, "Generated CREATE TABLE statement");
213 sql
214}
215
216fn generate_create_table(table: &TableInfo, dialect: Dialect) -> String {
218 generate_create_table_with_if_not_exists(table, dialect, true)
219}
220
221fn generate_drop_table(table_name: &str, dialect: Dialect) -> String {
223 tracing::debug!(table = %table_name, "Generating DROP TABLE DDL");
224 format!(
225 "DROP TABLE IF EXISTS {}",
226 quote_identifier(table_name, dialect)
227 )
228}
229
230fn generate_rename_table(from: &str, to: &str, dialect: Dialect) -> String {
232 tracing::debug!(from = %from, to = %to, "Generating RENAME TABLE DDL");
233 match dialect {
234 Dialect::Sqlite => format!(
235 "ALTER TABLE {} RENAME TO {}",
236 quote_identifier(from, dialect),
237 quote_identifier(to, dialect)
238 ),
239 Dialect::Mysql => format!(
240 "RENAME TABLE {} TO {}",
241 quote_identifier(from, dialect),
242 quote_identifier(to, dialect)
243 ),
244 Dialect::Postgres => format!(
245 "ALTER TABLE {} RENAME TO {}",
246 quote_identifier(from, dialect),
247 quote_identifier(to, dialect)
248 ),
249 }
250}
251
252fn generate_add_column(table: &str, column: &ColumnInfo, dialect: Dialect) -> String {
254 tracing::debug!(table = %table, column = %column.name, "Generating ADD COLUMN DDL");
255 format!(
256 "ALTER TABLE {} ADD COLUMN {}",
257 quote_identifier(table, dialect),
258 format_column_def(column, dialect)
259 )
260}
261
262fn generate_rename_column(table: &str, from: &str, to: &str, dialect: Dialect) -> String {
264 tracing::debug!(table = %table, from = %from, to = %to, "Generating RENAME COLUMN DDL");
265 match dialect {
266 Dialect::Sqlite => {
267 format!(
269 "ALTER TABLE {} RENAME COLUMN {} TO {}",
270 quote_identifier(table, dialect),
271 quote_identifier(from, dialect),
272 quote_identifier(to, dialect)
273 )
274 }
275 Dialect::Mysql => format!(
276 "ALTER TABLE {} RENAME COLUMN {} TO {}",
277 quote_identifier(table, dialect),
278 quote_identifier(from, dialect),
279 quote_identifier(to, dialect)
280 ),
281 Dialect::Postgres => format!(
282 "ALTER TABLE {} RENAME COLUMN {} TO {}",
283 quote_identifier(table, dialect),
284 quote_identifier(from, dialect),
285 quote_identifier(to, dialect)
286 ),
287 }
288}
289
290fn generate_create_index(table: &str, index: &IndexInfo, dialect: Dialect) -> String {
292 tracing::debug!(
293 table = %table,
294 index = %index.name,
295 columns = ?index.columns,
296 unique = index.unique,
297 "Generating CREATE INDEX DDL"
298 );
299
300 let unique = if index.unique { "UNIQUE " } else { "" };
301 let cols: Vec<String> = index
302 .columns
303 .iter()
304 .map(|c| quote_identifier(c, dialect))
305 .collect();
306
307 let using = match dialect {
309 Dialect::Postgres => {
310 if let Some(ref idx_type) = index.index_type {
311 format!(" USING {}", idx_type)
312 } else {
313 String::new()
314 }
315 }
316 Dialect::Mysql => {
317 if let Some(ref idx_type) = index.index_type {
318 if idx_type.eq_ignore_ascii_case("BTREE") {
319 String::new()
320 } else {
321 format!(" USING {}", idx_type)
322 }
323 } else {
324 String::new()
325 }
326 }
327 Dialect::Sqlite => String::new(),
328 };
329
330 format!(
331 "CREATE {}INDEX {} ON {}{}({})",
332 unique,
333 quote_identifier(&index.name, dialect),
334 quote_identifier(table, dialect),
335 using,
336 cols.join(", ")
337 )
338}
339
340fn generate_drop_index(table: &str, index_name: &str, dialect: Dialect) -> String {
342 tracing::debug!(table = %table, index = %index_name, "Generating DROP INDEX DDL");
343 match dialect {
344 Dialect::Sqlite => format!(
345 "DROP INDEX IF EXISTS {}",
346 quote_identifier(index_name, dialect)
347 ),
348 Dialect::Mysql => format!(
349 "DROP INDEX {} ON {}",
350 quote_identifier(index_name, dialect),
351 quote_identifier(table, dialect)
352 ),
353 Dialect::Postgres => format!(
354 "DROP INDEX IF EXISTS {}",
355 quote_identifier(index_name, dialect)
356 ),
357 }
358}
359
360#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::introspect::ParsedSqlType;
368
369 fn make_column(name: &str, sql_type: &str, nullable: bool) -> ColumnInfo {
370 ColumnInfo {
371 name: name.to_string(),
372 sql_type: sql_type.to_string(),
373 parsed_type: ParsedSqlType::parse(sql_type),
374 nullable,
375 default: None,
376 primary_key: false,
377 auto_increment: false,
378 comment: None,
379 }
380 }
381
382 fn make_table(name: &str, columns: Vec<ColumnInfo>, pk: Vec<&str>) -> TableInfo {
383 TableInfo {
384 name: name.to_string(),
385 columns,
386 primary_key: pk.into_iter().map(String::from).collect(),
387 foreign_keys: Vec::new(),
388 unique_constraints: Vec::new(),
389 check_constraints: Vec::new(),
390 indexes: Vec::new(),
391 comment: None,
392 }
393 }
394
395 #[test]
396 fn test_quote_identifier_sqlite() {
397 assert_eq!(quote_identifier("name", Dialect::Sqlite), "\"name\"");
398 assert_eq!(quote_identifier("table", Dialect::Sqlite), "\"table\"");
399 assert_eq!(
400 quote_identifier("col\"name", Dialect::Sqlite),
401 "\"col\"\"name\""
402 );
403 }
404
405 #[test]
406 fn test_quote_identifier_mysql() {
407 assert_eq!(quote_identifier("name", Dialect::Mysql), "`name`");
408 assert_eq!(quote_identifier("table", Dialect::Mysql), "`table`");
409 assert_eq!(quote_identifier("col`name", Dialect::Mysql), "`col``name`");
410 }
411
412 #[test]
413 fn test_format_column_def_basic() {
414 let col = make_column("name", "TEXT", false);
415 let def = format_column_def(&col, Dialect::Sqlite);
416 assert!(def.contains("\"name\""));
417 assert!(def.contains("TEXT"));
418 assert!(def.contains("NOT NULL"));
419 }
420
421 #[test]
422 fn test_format_column_def_nullable() {
423 let col = make_column("name", "TEXT", true);
424 let def = format_column_def(&col, Dialect::Sqlite);
425 assert!(!def.contains("NOT NULL"));
426 }
427
428 #[test]
429 fn test_format_column_def_with_default() {
430 let mut col = make_column("status", "TEXT", false);
431 col.default = Some("'active'".to_string());
432 let def = format_column_def(&col, Dialect::Sqlite);
433 assert!(def.contains("DEFAULT 'active'"));
434 }
435
436 #[test]
437 fn test_format_column_def_auto_increment_mysql() {
438 let mut col = make_column("id", "INT", false);
439 col.auto_increment = true;
440 let def = format_column_def(&col, Dialect::Mysql);
441 assert!(def.contains("AUTO_INCREMENT"));
442 }
443
444 #[test]
445 fn test_generate_create_table_basic() {
446 let table = make_table(
447 "heroes",
448 vec![
449 make_column("id", "INTEGER", false),
450 make_column("name", "TEXT", false),
451 ],
452 vec!["id"],
453 );
454 let sql = generate_create_table(&table, Dialect::Sqlite);
455 assert!(sql.contains("CREATE TABLE IF NOT EXISTS"));
456 assert!(sql.contains("\"heroes\""));
457 assert!(sql.contains("\"id\""));
458 assert!(sql.contains("\"name\""));
459 assert!(sql.contains("PRIMARY KEY"));
460 }
461
462 #[test]
463 fn test_generate_create_table_with_fk() {
464 let mut table = make_table(
465 "heroes",
466 vec![
467 make_column("id", "INTEGER", false),
468 make_column("team_id", "INTEGER", true),
469 ],
470 vec!["id"],
471 );
472 table.foreign_keys.push(ForeignKeyInfo {
473 name: Some("fk_heroes_team".to_string()),
474 column: "team_id".to_string(),
475 foreign_table: "teams".to_string(),
476 foreign_column: "id".to_string(),
477 on_delete: Some("CASCADE".to_string()),
478 on_update: None,
479 });
480
481 let sql = generate_create_table(&table, Dialect::Sqlite);
482 assert!(sql.contains("FOREIGN KEY"));
483 assert!(sql.contains("REFERENCES"));
484 assert!(sql.contains("ON DELETE CASCADE"));
485 }
486
487 #[test]
488 fn test_generate_drop_table() {
489 let sql = generate_drop_table("heroes", Dialect::Sqlite);
490 assert_eq!(sql, "DROP TABLE IF EXISTS \"heroes\"");
491 }
492
493 #[test]
494 fn test_generate_rename_table_sqlite() {
495 let sql = generate_rename_table("old_name", "new_name", Dialect::Sqlite);
496 assert!(sql.contains("ALTER TABLE"));
497 assert!(sql.contains("RENAME TO"));
498 }
499
500 #[test]
501 fn test_generate_rename_table_mysql() {
502 let sql = generate_rename_table("old_name", "new_name", Dialect::Mysql);
503 assert!(sql.contains("RENAME TABLE"));
504 }
505
506 #[test]
507 fn test_generate_add_column() {
508 let col = make_column("age", "INTEGER", true);
509 let sql = generate_add_column("heroes", &col, Dialect::Sqlite);
510 assert!(sql.contains("ALTER TABLE"));
511 assert!(sql.contains("ADD COLUMN"));
512 assert!(sql.contains("\"age\""));
513 }
514
515 #[test]
516 fn test_generate_rename_column() {
517 let sql = generate_rename_column("heroes", "old_name", "new_name", Dialect::Postgres);
518 assert!(sql.contains("ALTER TABLE"));
519 assert!(sql.contains("RENAME COLUMN"));
520 }
521
522 #[test]
523 fn test_generate_create_index() {
524 let index = IndexInfo {
525 name: "idx_heroes_name".to_string(),
526 columns: vec!["name".to_string()],
527 unique: false,
528 index_type: None,
529 primary: false,
530 };
531 let sql = generate_create_index("heroes", &index, Dialect::Sqlite);
532 assert!(sql.contains("CREATE INDEX"));
533 assert!(sql.contains("\"idx_heroes_name\""));
534 assert!(sql.contains("ON \"heroes\""));
535 }
536
537 #[test]
538 fn test_generate_create_unique_index() {
539 let index = IndexInfo {
540 name: "idx_heroes_name_unique".to_string(),
541 columns: vec!["name".to_string()],
542 unique: true,
543 index_type: None,
544 primary: false,
545 };
546 let sql = generate_create_index("heroes", &index, Dialect::Sqlite);
547 assert!(sql.contains("CREATE UNIQUE INDEX"));
548 }
549
550 #[test]
551 fn test_generate_drop_index_sqlite() {
552 let sql = generate_drop_index("heroes", "idx_heroes_name", Dialect::Sqlite);
553 assert_eq!(sql, "DROP INDEX IF EXISTS \"idx_heroes_name\"");
554 }
555
556 #[test]
557 fn test_generate_drop_index_mysql() {
558 let sql = generate_drop_index("heroes", "idx_heroes_name", Dialect::Mysql);
559 assert!(sql.contains("DROP INDEX"));
560 assert!(sql.contains("ON `heroes`"));
561 }
562
563 #[test]
564 fn test_generator_for_dialect() {
565 let sqlite = generator_for_dialect(Dialect::Sqlite);
566 assert_eq!(sqlite.dialect(), "sqlite");
567
568 let mysql = generator_for_dialect(Dialect::Mysql);
569 assert_eq!(mysql.dialect(), "mysql");
570
571 let postgres = generator_for_dialect(Dialect::Postgres);
572 assert_eq!(postgres.dialect(), "postgres");
573 }
574
575 #[test]
576 fn test_referential_action_formatting() {
577 assert_eq!(
578 format_referential_action(Some(&"CASCADE".to_string())),
579 "CASCADE"
580 );
581 assert_eq!(
582 format_referential_action(Some(&"cascade".to_string())),
583 "CASCADE"
584 );
585 assert_eq!(
586 format_referential_action(Some(&"SET NULL".to_string())),
587 "SET NULL"
588 );
589 assert_eq!(format_referential_action(None), "NO ACTION");
590 }
591}