use crate::error::Error;
use crate::model::{ColumnDef, DefaultValue, Schema, TableDef};
use crate::sql::{column_sql, composite_index_name, create_table_sql, index_name};
#[derive(Debug, Clone)]
pub struct Migration {
pub sql: String,
pub summary: String,
pub destructive: bool,
pub needs_manual_edit: bool,
}
impl Migration {
pub fn filename_slug(&self) -> String {
let mut slug = String::new();
for ch in self.summary.chars() {
if ch.is_ascii_alphanumeric() {
slug.push(ch.to_ascii_lowercase());
} else if !slug.ends_with('_') && !slug.is_empty() {
slug.push('_');
}
}
let slug = slug.trim_matches('_').to_string();
slug.chars().take(48).collect()
}
}
pub fn diff_schemas(old: &Schema, new: &Schema) -> Result<Option<Migration>, Error> {
let mut stmts: Vec<String> = Vec::new();
let mut summary: Vec<String> = Vec::new();
let mut destructive = false;
let mut needs_manual_edit = false;
for t in &new.tables {
if old.table(&t.name).is_none() {
stmts.push(create_table_sql(t));
stmts.extend(crate::sql::index_sqls(t));
stmts.extend(crate::sql::composite_index_sqls(t));
summary.push(format!("create {}", t.name));
}
}
for t in &old.tables {
if new.table(&t.name).is_none() {
stmts.push(format!("DROP TABLE {};", t.name));
summary.push(format!("drop {}", t.name));
destructive = true;
}
}
for t in &new.tables {
if let Some(old_t) = old.table(&t.name) {
diff_table(
old_t,
t,
&mut stmts,
&mut summary,
&mut destructive,
&mut needs_manual_edit,
)?;
}
}
if stmts.is_empty() {
return Ok(None);
}
Ok(Some(Migration {
sql: stmts.join("\n\n") + "\n",
summary: summary.join("; "),
destructive,
needs_manual_edit,
}))
}
fn diff_table(
old: &TableDef,
new: &TableDef,
stmts: &mut Vec<String>,
summary: &mut Vec<String>,
destructive: &mut bool,
needs_manual_edit: &mut bool,
) -> Result<(), Error> {
let mut eff = old.clone();
let mut renames: Vec<(String, String)> = Vec::new();
for c in &new.columns {
if let Some(from) = &c.renamed_from
&& eff.column(&c.name).is_none()
&& let Some(oc) = eff.columns.iter_mut().find(|oc| oc.name == *from)
{
oc.name = c.name.clone();
renames.push((from.clone(), c.name.clone()));
}
}
let added: Vec<&ColumnDef> = new
.columns
.iter()
.filter(|c| eff.column(&c.name).is_none())
.collect();
let dropped: Vec<ColumnDef> = eff
.columns
.iter()
.filter(|c| new.column(&c.name).is_none())
.cloned()
.collect();
let changed: Vec<String> = new
.columns
.iter()
.filter(|c| {
eff.column(&c.name)
.is_some_and(|oc| oc.signature() != c.signature())
})
.map(|c| c.name.clone())
.collect();
let index_changes: Vec<(&ColumnDef, bool)> = new
.columns
.iter()
.filter_map(|c| {
let old_c = eff.column(&c.name)?;
(old_c.index != c.index && old_c.signature() == c.signature())
.then_some((c, c.index))
})
.collect();
let unique_added: Vec<&Vec<String>> =
new.composite_uniques.iter().filter(|c| !old.composite_uniques.contains(c)).collect();
let unique_removed: Vec<&Vec<String>> =
old.composite_uniques.iter().filter(|c| !new.composite_uniques.contains(c)).collect();
let index_added: Vec<&Vec<String>> =
new.composite_indexes.iter().filter(|c| !old.composite_indexes.contains(c)).collect();
let index_removed: Vec<&Vec<String>> =
old.composite_indexes.iter().filter(|c| !new.composite_indexes.contains(c)).collect();
if renames.is_empty()
&& added.is_empty()
&& dropped.is_empty()
&& changed.is_empty()
&& index_changes.is_empty()
&& unique_added.is_empty()
&& unique_removed.is_empty()
&& index_added.is_empty()
&& index_removed.is_empty()
{
return Ok(());
}
let mut bits: Vec<String> = Vec::new();
bits.extend(renames.iter().map(|(f, t)| format!("rename {f} -> {t}")));
bits.extend(added.iter().map(|c| format!("add {}", c.name)));
bits.extend(dropped.iter().map(|c| format!("drop {}", c.name)));
bits.extend(changed.iter().map(|c| format!("change {c}")));
bits.extend(
index_changes
.iter()
.map(|(c, on)| format!("{} {}", if *on { "index" } else { "unindex" }, c.name)),
);
bits.extend(unique_added.iter().map(|c| format!("unique({})", c.join(","))));
bits.extend(unique_removed.iter().map(|c| format!("drop unique({})", c.join(","))));
bits.extend(index_added.iter().map(|c| format!("index({})", c.join(","))));
bits.extend(index_removed.iter().map(|c| format!("drop index({})", c.join(","))));
let rebuild = !changed.is_empty()
|| added.iter().any(|c| !can_add_column(c))
|| dropped.iter().any(|c| !can_drop_column(c));
if rebuild {
stmts.push(rebuild_table_sql(old, new, needs_manual_edit));
stmts.extend(crate::sql::index_sqls(new));
stmts.extend(crate::sql::composite_index_sqls(new));
summary.push(format!("rebuild {} ({})", new.name, bits.join(", ")));
} else {
for (from, to) in &renames {
if let Some(c) = new.column(to)
&& c.index
{
stmts.push(format!("DROP INDEX IF EXISTS {};", index_name(&new.name, from)));
}
stmts.push(format!("ALTER TABLE {} RENAME COLUMN {from} TO {to};", new.name));
if let Some(c) = new.column(to)
&& c.index
{
stmts.push(create_index_sql(&new.name, &c.name));
}
}
for c in &added {
stmts.push(format!("ALTER TABLE {} ADD COLUMN {};", new.name, column_sql(c, false)));
if c.index {
stmts.push(create_index_sql(&new.name, &c.name));
}
}
for c in &dropped {
stmts.push(format!("ALTER TABLE {} DROP COLUMN {};", new.name, c.name));
}
for (c, on) in &index_changes {
stmts.push(if *on {
create_index_sql(&new.name, &c.name)
} else {
format!("DROP INDEX IF EXISTS {};", index_name(&new.name, &c.name))
});
}
for cols in &unique_removed {
stmts.push(format!("DROP INDEX IF EXISTS {};", composite_index_name(&new.name, cols)));
}
for cols in &unique_added {
stmts.push(format!(
"CREATE UNIQUE INDEX {} ON {} ({});",
composite_index_name(&new.name, cols),
new.name,
cols.join(", "),
));
}
for cols in &index_removed {
stmts.push(format!("DROP INDEX IF EXISTS {};", composite_index_name(&new.name, cols)));
}
for cols in &index_added {
stmts.push(format!(
"CREATE INDEX {} ON {} ({});",
composite_index_name(&new.name, cols),
new.name,
cols.join(", "),
));
}
summary.push(format!("{}: {}", new.name, bits.join(", ")));
}
if !dropped.is_empty() {
*destructive = true;
}
Ok(())
}
fn create_index_sql(table: &str, column: &str) -> String {
format!("CREATE INDEX {} ON {table} ({column});", index_name(table, column))
}
fn can_add_column(c: &ColumnDef) -> bool {
let constant_default = matches!(&c.default, Some(d) if !matches!(d, DefaultValue::Now));
if c.primary_key || c.unique {
return false;
}
if c.references.is_some() {
return c.nullable && c.default.is_none();
}
c.nullable && !matches!(c.default, Some(DefaultValue::Now)) || constant_default
}
fn can_drop_column(c: &ColumnDef) -> bool {
!c.primary_key && !c.unique && !c.index
}
fn rebuild_table_sql(old: &TableDef, new: &TableDef, needs_manual_edit: &mut bool) -> String {
let table = &new.name;
let tmp_name = format!("{table}_new");
let tmp = TableDef {
name: tmp_name.clone(),
..new.clone()
};
let create = create_table_sql(&tmp);
let cols: Vec<String> = new.columns.iter().map(|c| c.name.clone()).collect();
let exprs: Vec<String> = new
.columns
.iter()
.map(|c| {
if let Some(from) = &c.renamed_from
&& old.column(from).is_some()
{
return from.clone();
}
if old.column(&c.name).is_some() {
return c.name.clone();
}
if let Some(d) = &c.default {
return d.sql();
}
if c.nullable {
return "NULL".into();
}
*needs_manual_edit = true;
format!("NULL /* TODO: backfill NOT NULL column {} */", c.name)
})
.collect();
format!(
"-- {table}: SQLite cannot express this change with ALTER TABLE, so the\n\
-- table is rebuilt and its rows copied over.\n\
{create}\n\n\
INSERT INTO {tmp_name} ({})\nSELECT {}\nFROM {table};\n\n\
DROP TABLE {table};\n\n\
ALTER TABLE {tmp_name} RENAME TO {table};",
cols.join(", "),
exprs.join(", "),
)
}