diff --git a/pg-worm/src/migration/mod.rs b/pg-worm/src/migration/mod.rs index 957b529..2f862a9 100644 --- a/pg-worm/src/migration/mod.rs +++ b/pg-worm/src/migration/mod.rs @@ -57,6 +57,46 @@ impl Table { self } + + fn up(&self) -> String { + let mut up = format!( + "CREATE TABLE {} ({})", + self.name, + self.columns.iter().map(|i| i.up())._join(", ") + ); + + if !self.constraints.is_empty() { + up.push_str(&format!( + ", {}", + self.constraints.iter().map(|i| i.up())._join(", ") + )); + } + + up + } + + fn down(&self) -> String { + format!("DROP TABLE IF EXISTS {}", self.name) + } + + fn drop_all_constraints_cascading(&self) -> String { + // This is a pl/pgsql code block which first queries for all + // constraints on a given table and then removes them. + format!( + r#"DO $$ + DECLARE i RECORD; + BEGIN + FOR i IN (SELECT conname + FROM pg_catalog.pg_constraint con + INNER JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid + INNER JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace + WHERE rel.relname = {0}) LOOP + EXECUTE format('ALTER TABLE {0} DROP CONSTRAINT %I CASCADE', i.conname); + END LOOP; + END $$;"#, + self.name + ) + } } impl Column { @@ -98,32 +138,7 @@ impl Column { self } -} - -trait Up { - fn up(&self) -> String; -} - -impl Up for Table { - fn up(&self) -> String { - let mut up = format!( - "CREATE TABLE {} ({})", - self.name, - self.columns.iter().map(|i| i.up())._join(", ") - ); - - if !self.constraints.is_empty() { - up.push_str(&format!( - ", {}", - self.constraints.iter().map(|i| i.up())._join(", ") - )); - } - - up - } -} -impl Up for Column { fn up(&self) -> String { let mut up = format!("{} {}", self.name, self.data_type); @@ -141,9 +156,40 @@ impl Up for Column { up } + + fn down(&self) -> String { + format!("DROP COLUMN IF EXISTS {}", self.name) + } + + fn migrate_from(&self, other: &Column, table: &Table) -> Vec { + let mut statements = Vec::new(); + + if other.constraints.len() > 0 { + statements.push(table.drop_all_constraints_cascading()); + } + + if self.constraints.len() > 0 { + let mut stmts = self + .constraints + .iter() + .map(|i| i.migrate_to(&self)) + .collect::>(); + + statements.append(&mut stmts); + } + + if self.data_type != other.data_type { + statements.push(format!( + "ALTER COLUMN {} SET TYPE {}", + self.name, self.data_type + )); + } + + statements + } } -impl Up for TableConstraint { +impl TableConstraint { fn up(&self) -> String { use TableConstraint as C; @@ -167,7 +213,7 @@ impl Up for TableConstraint { } } -impl Up for ColumnConstraint { +impl ColumnConstraint { fn up(&self) -> String { use ColumnConstraint as C; @@ -184,8 +230,21 @@ impl Up for ColumnConstraint { C::RawCheckNamed(name, check) => format!("CONSTRAINT {name} CHECK ({check})"), } } + + fn migrate_to(&self, column: &Column) -> String { + use ColumnConstraint as C; + + match self { + C::NotNull => format!("ALTER COLUMN {} DROP NULL", column.name), + C::PrimaryKey => format!("ADD CONSTRAINT PRIMARY KEY ({})", column.name), + C::Unique => format!("ALTER COLUMN {} SET UNIQUE", column.name), + _ => todo!(), + } + } } +/// Convenience method to avoid writing `.iter().map(|i| i.to_string()).collect::>().join()` +/// all the time. trait Join { fn _join(self, _: &str) -> String; } @@ -206,8 +265,6 @@ where #[cfg(test)] mod tests { - use crate::migration::Up; - use super::{Column, Table}; #[test]