1use std::collections::HashMap;
2
3use crate::query::{AlterTable, Update};
4use anyhow::Result;
5
6use crate::query::AlterAction;
7use crate::query::CreateIndex;
8use crate::query::CreateTable;
9use crate::query::DropTable;
10use crate::schema::Schema;
11use crate::{Dialect, ToSql};
12
13#[derive(Debug, Clone, Default)]
14pub struct MigrationOptions {
15 pub debug: bool,
16 pub allow_destructive: bool,
17}
18
19pub fn migrate(current: Schema, desired: Schema, options: &MigrationOptions) -> Result<Migration> {
20 let current_tables = current
21 .tables
22 .iter()
23 .map(|t| (&t.name, t))
24 .collect::<HashMap<_, _>>();
25 let desired_tables = desired
26 .tables
27 .iter()
28 .map(|t| (&t.name, t))
29 .collect::<HashMap<_, _>>();
30
31 let mut debug_results = vec![];
32 let mut statements = Vec::new();
33 for (_name, table) in desired_tables
35 .iter()
36 .filter(|(name, _)| !current_tables.contains_key(*name))
37 {
38 let statement = Statement::CreateTable(CreateTable::from_table(table));
39 statements.push(statement);
40 }
41
42 for (name, desired_table) in desired_tables
44 .iter()
45 .filter(|(name, _)| current_tables.contains_key(*name))
46 {
47 let current_table = current_tables[name];
48 let current_columns = current_table
49 .columns
50 .iter()
51 .map(|c| (&c.name, c))
52 .collect::<HashMap<_, _>>();
53 let mut actions = vec![];
55 for desired_column in desired_table.columns.iter() {
56 if let Some(current) = current_columns.get(&desired_column.name) {
57 if current.nullable != desired_column.nullable {
58 actions.push(AlterAction::set_nullable(
59 desired_column.name.clone(),
60 desired_column.nullable,
61 ));
62 }
63 if !desired_column.typ.lossy_eq(¤t.typ) {
64 actions.push(AlterAction::set_type(
65 desired_column.name.clone(),
66 desired_column.typ.clone(),
67 ));
68 };
69 if desired_column.constraint != current.constraint {
70 if let Some(c) = &desired_column.constraint {
71 let name = desired_column.name.clone();
72 actions.push(AlterAction::add_constraint(&desired_table.name, name, c.clone()));
73 }
74 }
75 } else {
76 if desired_column.nullable {
78 actions.push(AlterAction::AddColumn {
79 column: desired_column.clone(),
80 });
81 } else {
82 let mut nullable = desired_column.clone();
83 nullable.nullable = true;
84 statements.push(Statement::AlterTable(AlterTable {
85 schema: desired_table.schema.clone(),
86 name: desired_table.name.clone(),
87 actions: vec![AlterAction::AddColumn { column: nullable }],
88 }));
89 statements.push(Statement::Update(
90 Update::new(name)
91 .set(
92 &desired_column.name,
93 "/* TODO set a value before setting the column to null */",
94 )
95 .where_(crate::query::Where::raw("true")),
96 ));
97 statements.push(Statement::AlterTable(AlterTable {
98 schema: desired_table.schema.clone(),
99 name: desired_table.name.clone(),
100 actions: vec![AlterAction::AlterColumn {
101 name: desired_column.name.clone(),
102 action: crate::query::AlterColumnAction::SetNullable(false),
103 }],
104 }));
105 }
106 }
107 }
108 if actions.is_empty() {
109 debug_results.push(DebugResults::TablesIdentical(name.to_string()));
110 } else {
111 statements.push(Statement::AlterTable(AlterTable {
112 schema: desired_table.schema.clone(),
113 name: desired_table.name.clone(),
114 actions,
115 }));
116 }
117 }
118
119 for (_name, current_table) in current_tables
120 .iter()
121 .filter(|(name, _)| !desired_tables.contains_key(*name))
122 {
123 if options.allow_destructive {
124 statements.push(Statement::DropTable(DropTable {
125 schema: current_table.schema.clone(),
126 name: current_table.name.clone(),
127 }));
128 } else {
129 debug_results.push(DebugResults::SkippedDropTable(current_table.name.clone()));
130 }
131 }
132
133 Ok(Migration {
134 statements,
135 debug_results,
136 })
137}
138
139#[derive(Debug)]
140pub struct Migration {
141 pub statements: Vec<Statement>,
142 pub debug_results: Vec<DebugResults>,
143}
144
145impl Migration {
146 pub fn is_empty(&self) -> bool {
147 self.statements.is_empty()
148 }
149
150 pub fn set_schema(&mut self, schema_name: &str) {
151 for statement in &mut self.statements {
152 statement.set_schema(schema_name);
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum Statement {
159 CreateTable(CreateTable),
160 CreateIndex(CreateIndex),
161 AlterTable(AlterTable),
162 DropTable(DropTable),
163 Update(Update),
164}
165
166impl Statement {
167 pub fn set_schema(&mut self, schema_name: &str) {
168 match self {
169 Statement::CreateTable(s) => {
170 s.schema = Some(schema_name.to_string());
171 }
172 Statement::AlterTable(s) => {
173 s.schema = Some(schema_name.to_string());
174 }
175 Statement::DropTable(s) => {
176 s.schema = Some(schema_name.to_string());
177 }
178 Statement::CreateIndex(s) => {
179 s.schema = Some(schema_name.to_string());
180 }
181 Statement::Update(s) => {
182 s.schema = Some(schema_name.to_string());
183 }
184 }
185 }
186
187 pub fn table_name(&self) -> &str {
188 match self {
189 Statement::CreateTable(s) => &s.name,
190 Statement::AlterTable(s) => &s.name,
191 Statement::DropTable(s) => &s.name,
192 Statement::CreateIndex(s) => &s.table,
193 Statement::Update(s) => &s.table,
194 }
195 }
196}
197
198impl ToSql for Statement {
199 fn write_sql(&self, buf: &mut String, dialect: Dialect) {
200 use Statement::*;
201 match self {
202 CreateTable(c) => c.write_sql(buf, dialect),
203 CreateIndex(c) => c.write_sql(buf, dialect),
204 AlterTable(a) => a.write_sql(buf, dialect),
205 DropTable(d) => d.write_sql(buf, dialect),
206 Update(u) => u.write_sql(buf, dialect),
207 }
208 }
209}
210
211#[derive(Debug)]
212pub enum DebugResults {
213 TablesIdentical(String),
214 SkippedDropTable(String),
215}
216
217impl DebugResults {
218 pub fn table_name(&self) -> &str {
219 match self {
220 DebugResults::TablesIdentical(name) => name,
221 DebugResults::SkippedDropTable(name) => name,
222 }
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 use crate::Table;
231
232 #[test]
233 fn test_drop_table() {
234 let empty_schema = Schema::default();
235 let mut single_table_schema = Schema::default();
236 let t = Table::new("new_table");
237 single_table_schema.tables.push(t.clone());
238 let mut allow_destructive_options = MigrationOptions::default();
239 allow_destructive_options.allow_destructive = true;
240
241 let mut migrations = migrate(
242 single_table_schema,
243 empty_schema,
244 &allow_destructive_options,
245 )
246 .unwrap();
247
248 let statement = migrations.statements.pop().unwrap();
249 let expected_statement = Statement::DropTable(DropTable {
250 schema: t.schema,
251 name: t.name,
252 });
253
254 assert_eq!(statement, expected_statement);
255 }
256
257 #[test]
258 fn test_drop_table_without_destructive_operations() {
259 let empty_schema = Schema::default();
260 let mut single_table_schema = Schema::default();
261 let t = Table::new("new_table");
262 single_table_schema.tables.push(t.clone());
263 let options = MigrationOptions::default();
264
265 let migrations = migrate(single_table_schema, empty_schema, &options).unwrap();
266 assert!(migrations.statements.is_empty());
267 }
268}