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(table: &TableInfo, dialect: Dialect) -> String {
158 tracing::debug!(
159 dialect = %match dialect {
160 Dialect::Sqlite => "sqlite",
161 Dialect::Mysql => "mysql",
162 Dialect::Postgres => "postgres",
163 },
164 table = %table.name,
165 columns = table.columns.len(),
166 "Generating CREATE TABLE DDL"
167 );
168
169 let mut parts = Vec::new();
170
171 for col in &table.columns {
173 parts.push(format!(" {}", format_column_def(col, dialect)));
174 }
175
176 if !table.primary_key.is_empty() {
178 let pk_cols: Vec<String> = table
179 .primary_key
180 .iter()
181 .map(|c| quote_identifier(c, dialect))
182 .collect();
183 parts.push(format!(" PRIMARY KEY ({})", pk_cols.join(", ")));
184 }
185
186 for unique in &table.unique_constraints {
188 parts.push(format!(" {}", format_unique_constraint(unique, dialect)));
189 }
190
191 for fk in &table.foreign_keys {
193 parts.push(format!(" {}", format_fk_constraint(fk, dialect)));
194 }
195
196 let table_name = quote_identifier(&table.name, dialect);
197 let sql = format!(
198 "CREATE TABLE IF NOT EXISTS {} (\n{}\n)",
199 table_name,
200 parts.join(",\n")
201 );
202
203 tracing::trace!(sql = %sql, "Generated CREATE TABLE statement");
204 sql
205}
206
207fn generate_drop_table(table_name: &str, dialect: Dialect) -> String {
209 tracing::debug!(table = %table_name, "Generating DROP TABLE DDL");
210 format!(
211 "DROP TABLE IF EXISTS {}",
212 quote_identifier(table_name, dialect)
213 )
214}
215
216fn generate_rename_table(from: &str, to: &str, dialect: Dialect) -> String {
218 tracing::debug!(from = %from, to = %to, "Generating RENAME TABLE DDL");
219 match dialect {
220 Dialect::Sqlite => format!(
221 "ALTER TABLE {} RENAME TO {}",
222 quote_identifier(from, dialect),
223 quote_identifier(to, dialect)
224 ),
225 Dialect::Mysql => format!(
226 "RENAME TABLE {} TO {}",
227 quote_identifier(from, dialect),
228 quote_identifier(to, dialect)
229 ),
230 Dialect::Postgres => format!(
231 "ALTER TABLE {} RENAME TO {}",
232 quote_identifier(from, dialect),
233 quote_identifier(to, dialect)
234 ),
235 }
236}
237
238fn generate_add_column(table: &str, column: &ColumnInfo, dialect: Dialect) -> String {
240 tracing::debug!(table = %table, column = %column.name, "Generating ADD COLUMN DDL");
241 format!(
242 "ALTER TABLE {} ADD COLUMN {}",
243 quote_identifier(table, dialect),
244 format_column_def(column, dialect)
245 )
246}
247
248fn generate_rename_column(table: &str, from: &str, to: &str, dialect: Dialect) -> String {
250 tracing::debug!(table = %table, from = %from, to = %to, "Generating RENAME COLUMN DDL");
251 match dialect {
252 Dialect::Sqlite => {
253 format!(
255 "ALTER TABLE {} RENAME COLUMN {} TO {}",
256 quote_identifier(table, dialect),
257 quote_identifier(from, dialect),
258 quote_identifier(to, dialect)
259 )
260 }
261 Dialect::Mysql => format!(
262 "ALTER TABLE {} RENAME COLUMN {} TO {}",
263 quote_identifier(table, dialect),
264 quote_identifier(from, dialect),
265 quote_identifier(to, dialect)
266 ),
267 Dialect::Postgres => format!(
268 "ALTER TABLE {} RENAME COLUMN {} TO {}",
269 quote_identifier(table, dialect),
270 quote_identifier(from, dialect),
271 quote_identifier(to, dialect)
272 ),
273 }
274}
275
276fn generate_create_index(table: &str, index: &IndexInfo, dialect: Dialect) -> String {
278 tracing::debug!(
279 table = %table,
280 index = %index.name,
281 columns = ?index.columns,
282 unique = index.unique,
283 "Generating CREATE INDEX DDL"
284 );
285
286 let unique = if index.unique { "UNIQUE " } else { "" };
287 let cols: Vec<String> = index
288 .columns
289 .iter()
290 .map(|c| quote_identifier(c, dialect))
291 .collect();
292
293 let using = match dialect {
295 Dialect::Postgres => {
296 if let Some(ref idx_type) = index.index_type {
297 format!(" USING {}", idx_type)
298 } else {
299 String::new()
300 }
301 }
302 Dialect::Mysql => {
303 if let Some(ref idx_type) = index.index_type {
304 if idx_type.eq_ignore_ascii_case("BTREE") {
305 String::new()
306 } else {
307 format!(" USING {}", idx_type)
308 }
309 } else {
310 String::new()
311 }
312 }
313 Dialect::Sqlite => String::new(),
314 };
315
316 format!(
317 "CREATE {}INDEX {} ON {}{}({})",
318 unique,
319 quote_identifier(&index.name, dialect),
320 quote_identifier(table, dialect),
321 using,
322 cols.join(", ")
323 )
324}
325
326fn generate_drop_index(table: &str, index_name: &str, dialect: Dialect) -> String {
328 tracing::debug!(table = %table, index = %index_name, "Generating DROP INDEX DDL");
329 match dialect {
330 Dialect::Sqlite => format!(
331 "DROP INDEX IF EXISTS {}",
332 quote_identifier(index_name, dialect)
333 ),
334 Dialect::Mysql => format!(
335 "DROP INDEX {} ON {}",
336 quote_identifier(index_name, dialect),
337 quote_identifier(table, dialect)
338 ),
339 Dialect::Postgres => format!(
340 "DROP INDEX IF EXISTS {}",
341 quote_identifier(index_name, dialect)
342 ),
343 }
344}
345
346#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::introspect::ParsedSqlType;
354
355 fn make_column(name: &str, sql_type: &str, nullable: bool) -> ColumnInfo {
356 ColumnInfo {
357 name: name.to_string(),
358 sql_type: sql_type.to_string(),
359 parsed_type: ParsedSqlType::parse(sql_type),
360 nullable,
361 default: None,
362 primary_key: false,
363 auto_increment: false,
364 comment: None,
365 }
366 }
367
368 fn make_table(name: &str, columns: Vec<ColumnInfo>, pk: Vec<&str>) -> TableInfo {
369 TableInfo {
370 name: name.to_string(),
371 columns,
372 primary_key: pk.into_iter().map(String::from).collect(),
373 foreign_keys: Vec::new(),
374 unique_constraints: Vec::new(),
375 check_constraints: Vec::new(),
376 indexes: Vec::new(),
377 comment: None,
378 }
379 }
380
381 #[test]
382 fn test_quote_identifier_sqlite() {
383 assert_eq!(quote_identifier("name", Dialect::Sqlite), "\"name\"");
384 assert_eq!(quote_identifier("table", Dialect::Sqlite), "\"table\"");
385 assert_eq!(
386 quote_identifier("col\"name", Dialect::Sqlite),
387 "\"col\"\"name\""
388 );
389 }
390
391 #[test]
392 fn test_quote_identifier_mysql() {
393 assert_eq!(quote_identifier("name", Dialect::Mysql), "`name`");
394 assert_eq!(quote_identifier("table", Dialect::Mysql), "`table`");
395 assert_eq!(quote_identifier("col`name", Dialect::Mysql), "`col``name`");
396 }
397
398 #[test]
399 fn test_format_column_def_basic() {
400 let col = make_column("name", "TEXT", false);
401 let def = format_column_def(&col, Dialect::Sqlite);
402 assert!(def.contains("\"name\""));
403 assert!(def.contains("TEXT"));
404 assert!(def.contains("NOT NULL"));
405 }
406
407 #[test]
408 fn test_format_column_def_nullable() {
409 let col = make_column("name", "TEXT", true);
410 let def = format_column_def(&col, Dialect::Sqlite);
411 assert!(!def.contains("NOT NULL"));
412 }
413
414 #[test]
415 fn test_format_column_def_with_default() {
416 let mut col = make_column("status", "TEXT", false);
417 col.default = Some("'active'".to_string());
418 let def = format_column_def(&col, Dialect::Sqlite);
419 assert!(def.contains("DEFAULT 'active'"));
420 }
421
422 #[test]
423 fn test_format_column_def_auto_increment_mysql() {
424 let mut col = make_column("id", "INT", false);
425 col.auto_increment = true;
426 let def = format_column_def(&col, Dialect::Mysql);
427 assert!(def.contains("AUTO_INCREMENT"));
428 }
429
430 #[test]
431 fn test_generate_create_table_basic() {
432 let table = make_table(
433 "heroes",
434 vec![
435 make_column("id", "INTEGER", false),
436 make_column("name", "TEXT", false),
437 ],
438 vec!["id"],
439 );
440 let sql = generate_create_table(&table, Dialect::Sqlite);
441 assert!(sql.contains("CREATE TABLE IF NOT EXISTS"));
442 assert!(sql.contains("\"heroes\""));
443 assert!(sql.contains("\"id\""));
444 assert!(sql.contains("\"name\""));
445 assert!(sql.contains("PRIMARY KEY"));
446 }
447
448 #[test]
449 fn test_generate_create_table_with_fk() {
450 let mut table = make_table(
451 "heroes",
452 vec![
453 make_column("id", "INTEGER", false),
454 make_column("team_id", "INTEGER", true),
455 ],
456 vec!["id"],
457 );
458 table.foreign_keys.push(ForeignKeyInfo {
459 name: Some("fk_heroes_team".to_string()),
460 column: "team_id".to_string(),
461 foreign_table: "teams".to_string(),
462 foreign_column: "id".to_string(),
463 on_delete: Some("CASCADE".to_string()),
464 on_update: None,
465 });
466
467 let sql = generate_create_table(&table, Dialect::Sqlite);
468 assert!(sql.contains("FOREIGN KEY"));
469 assert!(sql.contains("REFERENCES"));
470 assert!(sql.contains("ON DELETE CASCADE"));
471 }
472
473 #[test]
474 fn test_generate_drop_table() {
475 let sql = generate_drop_table("heroes", Dialect::Sqlite);
476 assert_eq!(sql, "DROP TABLE IF EXISTS \"heroes\"");
477 }
478
479 #[test]
480 fn test_generate_rename_table_sqlite() {
481 let sql = generate_rename_table("old_name", "new_name", Dialect::Sqlite);
482 assert!(sql.contains("ALTER TABLE"));
483 assert!(sql.contains("RENAME TO"));
484 }
485
486 #[test]
487 fn test_generate_rename_table_mysql() {
488 let sql = generate_rename_table("old_name", "new_name", Dialect::Mysql);
489 assert!(sql.contains("RENAME TABLE"));
490 }
491
492 #[test]
493 fn test_generate_add_column() {
494 let col = make_column("age", "INTEGER", true);
495 let sql = generate_add_column("heroes", &col, Dialect::Sqlite);
496 assert!(sql.contains("ALTER TABLE"));
497 assert!(sql.contains("ADD COLUMN"));
498 assert!(sql.contains("\"age\""));
499 }
500
501 #[test]
502 fn test_generate_rename_column() {
503 let sql = generate_rename_column("heroes", "old_name", "new_name", Dialect::Postgres);
504 assert!(sql.contains("ALTER TABLE"));
505 assert!(sql.contains("RENAME COLUMN"));
506 }
507
508 #[test]
509 fn test_generate_create_index() {
510 let index = IndexInfo {
511 name: "idx_heroes_name".to_string(),
512 columns: vec!["name".to_string()],
513 unique: false,
514 index_type: None,
515 primary: false,
516 };
517 let sql = generate_create_index("heroes", &index, Dialect::Sqlite);
518 assert!(sql.contains("CREATE INDEX"));
519 assert!(sql.contains("\"idx_heroes_name\""));
520 assert!(sql.contains("ON \"heroes\""));
521 }
522
523 #[test]
524 fn test_generate_create_unique_index() {
525 let index = IndexInfo {
526 name: "idx_heroes_name_unique".to_string(),
527 columns: vec!["name".to_string()],
528 unique: true,
529 index_type: None,
530 primary: false,
531 };
532 let sql = generate_create_index("heroes", &index, Dialect::Sqlite);
533 assert!(sql.contains("CREATE UNIQUE INDEX"));
534 }
535
536 #[test]
537 fn test_generate_drop_index_sqlite() {
538 let sql = generate_drop_index("heroes", "idx_heroes_name", Dialect::Sqlite);
539 assert_eq!(sql, "DROP INDEX IF EXISTS \"idx_heroes_name\"");
540 }
541
542 #[test]
543 fn test_generate_drop_index_mysql() {
544 let sql = generate_drop_index("heroes", "idx_heroes_name", Dialect::Mysql);
545 assert!(sql.contains("DROP INDEX"));
546 assert!(sql.contains("ON `heroes`"));
547 }
548
549 #[test]
550 fn test_generator_for_dialect() {
551 let sqlite = generator_for_dialect(Dialect::Sqlite);
552 assert_eq!(sqlite.dialect(), "sqlite");
553
554 let mysql = generator_for_dialect(Dialect::Mysql);
555 assert_eq!(mysql.dialect(), "mysql");
556
557 let postgres = generator_for_dialect(Dialect::Postgres);
558 assert_eq!(postgres.dialect(), "postgres");
559 }
560
561 #[test]
562 fn test_referential_action_formatting() {
563 assert_eq!(
564 format_referential_action(Some(&"CASCADE".to_string())),
565 "CASCADE"
566 );
567 assert_eq!(
568 format_referential_action(Some(&"cascade".to_string())),
569 "CASCADE"
570 );
571 assert_eq!(
572 format_referential_action(Some(&"SET NULL".to_string())),
573 "SET NULL"
574 );
575 assert_eq!(format_referential_action(None), "NO ACTION");
576 }
577}