use crate::differ::DiffResult;
use crate::parser::SqlDialect;
pub fn format_sql(result: &DiffResult, dialect: SqlDialect) -> String {
let mut output = String::new();
output.push_str("-- SQL Migration Script\n");
output.push_str(&format!(
"-- Generated by sql-splitter diff (dialect: {})\n",
dialect
));
output.push_str("--\n\n");
let Some(ref schema) = result.schema else {
output.push_str("-- No schema changes detected\n");
return output;
};
if !schema.has_changes() {
output.push_str("-- No schema changes detected\n");
return output;
}
let quote = |name: &str| -> String {
match dialect {
SqlDialect::MySql => format!("`{}`", name),
SqlDialect::Postgres | SqlDialect::Sqlite => format!("\"{}\"", name),
SqlDialect::Mssql => format!("[{}]", name),
}
};
for table in &schema.tables_added {
output.push_str(&format!("-- New table: {}\n", table.name));
if let Some(ref stmt) = table.create_statement {
output.push_str(stmt.trim());
if !stmt.trim().ends_with(';') {
output.push(';');
}
output.push_str("\n\n");
} else {
output.push_str(&format!("CREATE TABLE {} (\n", quote(&table.name)));
let col_defs: Vec<String> = table
.columns
.iter()
.map(|col| {
let nullable = if col.is_nullable { "" } else { " NOT NULL" };
format!(" {} {}{}", quote(&col.name), col.col_type, nullable)
})
.collect();
output.push_str(&col_defs.join(",\n"));
if !table.primary_key.is_empty() {
let pk_cols: Vec<String> = table.primary_key.iter().map(|c| quote(c)).collect();
output.push_str(&format!(",\n PRIMARY KEY ({})", pk_cols.join(", ")));
}
output.push_str("\n);\n\n");
}
}
for table_name in &schema.tables_removed {
output.push_str(&format!("-- Removed table: {}\n", table_name));
output.push_str(&format!("DROP TABLE IF EXISTS {};\n\n", quote(table_name)));
}
for modification in &schema.tables_modified {
let has_changes = !modification.columns_added.is_empty()
|| !modification.columns_removed.is_empty()
|| !modification.columns_modified.is_empty()
|| !modification.fks_added.is_empty()
|| !modification.fks_removed.is_empty()
|| !modification.indexes_added.is_empty()
|| !modification.indexes_removed.is_empty();
if !has_changes && !modification.pk_changed {
continue;
}
output.push_str(&format!("-- Modified table: {}\n", modification.table_name));
for col in &modification.columns_added {
let nullable = if col.is_nullable { "" } else { " NOT NULL" };
output.push_str(&format!(
"ALTER TABLE {} ADD COLUMN {} {}{};\n",
quote(&modification.table_name),
quote(&col.name),
col.col_type,
nullable
));
}
for col in &modification.columns_removed {
output.push_str(&format!(
"ALTER TABLE {} DROP COLUMN {};\n",
quote(&modification.table_name),
quote(&col.name)
));
}
for change in &modification.columns_modified {
let new_type = change.new_type.as_deref().unwrap_or("-- unknown type");
let nullable = match change.new_nullable {
Some(true) => "",
Some(false) => " NOT NULL",
None => "",
};
match dialect {
SqlDialect::MySql => {
output.push_str(&format!(
"ALTER TABLE {} MODIFY COLUMN {} {}{};\n",
quote(&modification.table_name),
quote(&change.name),
new_type,
nullable
));
}
SqlDialect::Postgres => {
if change.new_type.is_some() {
output.push_str(&format!(
"ALTER TABLE {} ALTER COLUMN {} TYPE {};\n",
quote(&modification.table_name),
quote(&change.name),
new_type
));
}
if let Some(is_nullable) = change.new_nullable {
let constraint = if is_nullable {
"DROP NOT NULL"
} else {
"SET NOT NULL"
};
output.push_str(&format!(
"ALTER TABLE {} ALTER COLUMN {} {};\n",
quote(&modification.table_name),
quote(&change.name),
constraint
));
}
}
SqlDialect::Sqlite => {
output.push_str(&format!(
"-- SQLite does not support ALTER COLUMN; manual migration required for {}.{}\n",
modification.table_name, change.name
));
}
SqlDialect::Mssql => {
output.push_str(&format!(
"ALTER TABLE {} ALTER COLUMN {} {}{};\n",
quote(&modification.table_name),
quote(&change.name),
new_type,
nullable
));
}
}
}
for fk in &modification.fks_added {
let fk_cols: Vec<String> = fk.columns.iter().map(|c| quote(c)).collect();
let ref_cols: Vec<String> = fk.referenced_columns.iter().map(|c| quote(c)).collect();
let constraint_name = fk
.name
.as_ref()
.map(|n| format!("CONSTRAINT {} ", quote(n)))
.unwrap_or_default();
output.push_str(&format!(
"ALTER TABLE {} ADD {}FOREIGN KEY ({}) REFERENCES {}({});\n",
quote(&modification.table_name),
constraint_name,
fk_cols.join(", "),
quote(&fk.referenced_table),
ref_cols.join(", ")
));
}
for fk in &modification.fks_removed {
if let Some(ref name) = fk.name {
match dialect {
SqlDialect::MySql => {
output.push_str(&format!(
"ALTER TABLE {} DROP FOREIGN KEY {};\n",
quote(&modification.table_name),
quote(name)
));
}
SqlDialect::Postgres | SqlDialect::Mssql => {
output.push_str(&format!(
"ALTER TABLE {} DROP CONSTRAINT {};\n",
quote(&modification.table_name),
quote(name)
));
}
SqlDialect::Sqlite => {
output.push_str(&format!(
"-- SQLite does not support DROP CONSTRAINT; manual migration required for FK {}\n",
name
));
}
}
} else {
output.push_str(&format!(
"-- Cannot drop unnamed FK ({}) -> {}.({}) without constraint name\n",
fk.columns.join(", "),
fk.referenced_table,
fk.referenced_columns.join(", ")
));
}
}
for idx in &modification.indexes_added {
let unique = if idx.is_unique { "UNIQUE " } else { "" };
let idx_cols: Vec<String> = idx.columns.iter().map(|c| quote(c)).collect();
match dialect {
SqlDialect::Postgres => {
let using = idx
.index_type
.as_ref()
.map(|t| format!(" USING {}", t.to_lowercase()))
.unwrap_or_default();
output.push_str(&format!(
"CREATE {}INDEX {} ON {}{}({});\n",
unique,
quote(&idx.name),
quote(&modification.table_name),
using,
idx_cols.join(", ")
));
}
_ => {
output.push_str(&format!(
"CREATE {}INDEX {} ON {}({});\n",
unique,
quote(&idx.name),
quote(&modification.table_name),
idx_cols.join(", ")
));
}
}
}
for idx in &modification.indexes_removed {
match dialect {
SqlDialect::MySql => {
output.push_str(&format!(
"DROP INDEX {} ON {};\n",
quote(&idx.name),
quote(&modification.table_name)
));
}
SqlDialect::Postgres | SqlDialect::Sqlite => {
output.push_str(&format!("DROP INDEX IF EXISTS {};\n", quote(&idx.name)));
}
SqlDialect::Mssql => {
output.push_str(&format!(
"DROP INDEX {} ON {};\n",
quote(&idx.name),
quote(&modification.table_name)
));
}
}
}
if modification.pk_changed {
let old_pk = modification
.old_pk
.as_ref()
.map(|pk| pk.join(", "))
.unwrap_or_else(|| "(none)".to_string());
let new_pk = modification
.new_pk
.as_ref()
.map(|pk| pk.join(", "))
.unwrap_or_else(|| "(none)".to_string());
output.push_str(&format!(
"-- WARNING: PRIMARY KEY change ({}) -> ({}) may require table recreation\n",
old_pk, new_pk
));
match dialect {
SqlDialect::MySql => {
output.push_str(&format!(
"ALTER TABLE {} DROP PRIMARY KEY;\n",
quote(&modification.table_name)
));
if let Some(ref new_pk_cols) = modification.new_pk {
if !new_pk_cols.is_empty() {
let pk_cols: Vec<String> =
new_pk_cols.iter().map(|c| quote(c)).collect();
output.push_str(&format!(
"ALTER TABLE {} ADD PRIMARY KEY ({});\n",
quote(&modification.table_name),
pk_cols.join(", ")
));
}
}
}
SqlDialect::Postgres | SqlDialect::Mssql => {
output.push_str(&format!(
"ALTER TABLE {} DROP CONSTRAINT IF EXISTS {}_pkey;\n",
quote(&modification.table_name),
modification.table_name
));
if let Some(ref new_pk_cols) = modification.new_pk {
if !new_pk_cols.is_empty() {
let pk_cols: Vec<String> =
new_pk_cols.iter().map(|c| quote(c)).collect();
output.push_str(&format!(
"ALTER TABLE {} ADD PRIMARY KEY ({});\n",
quote(&modification.table_name),
pk_cols.join(", ")
));
}
}
}
SqlDialect::Sqlite => {
output.push_str(
"-- SQLite does not support ALTER PRIMARY KEY; table recreation required\n",
);
}
}
}
output.push('\n');
}
if let Some(ref data) = result.data {
let has_data_changes = data
.tables
.values()
.any(|t| t.added_count > 0 || t.removed_count > 0 || t.modified_count > 0);
if has_data_changes {
output.push_str("-- Data changes detected but not included in migration script\n");
output.push_str("-- Use text or JSON output format for data diff details\n");
}
}
output
}