1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
use std::collections::HashMap;
use log::{warn, debug};
use crate::{connection::Connection, IntoSqlite};
use super::{Model, column::Column};
/// Migrator ensures that the database is up to date with the latest schema.
///
/// This is done by comparing the latest schema with the current schema and updating the database as needed.
/// There is no rollback support yet, so if the migration fails, the database will be in an inconsistent state.
pub struct Migrator;
impl Migrator {
/// Migrate the database to the latest schema.
pub fn migrate(latest_schema: &DbSchema, connection: &Connection) {
// Compare the latest schema with the current schema updating the database as needed.
Self::migrate_models(latest_schema, connection);
}
#[allow(unreachable_code)]
pub fn migrate_models(latest_schema: &DbSchema, connection: &Connection) {
// Iterate over the tables in database and compare them to the latest schema.
// If the table is not in the latest schema, drop it.
// If the table is in the latest schema, compare the columns.
// If the column is not in the database, add it.
let tables = connection.get_all_tables().unwrap();
for table in tables.iter() {
if latest_schema.tables.get(&table.clone()).is_some() {
// The table is in the latest schema, compare the columns.
let columns = connection.get_all_columns(table).unwrap();
// Remove columns that are not in the latest schema.
for column in columns.iter() {
if !latest_schema.tables.get(&table.clone()).unwrap().iter().any(|c| c.name() == column.name()) {
// The column is not in the latest schema, drop it.
// safety note: this is safe because the column name is checked against the latest schema.
connection.execute_no_params(&format!(
"ALTER TABLE {} DROP COLUMN {};",
table, column.name()
)).unwrap();
warn!(target: "migration", "Dropped column {} from table {}.", column.name(), table);
}
}
for latest_column in latest_schema.tables.get(&table.clone()).unwrap().iter() {
// Change existing columns.
if !columns.iter().any(|c| c.name() == latest_column.name()) {
// The column is not in the latest schema, add it without modifying the data.
// safety note: this is safe because the column name is checked against the latest schema.
connection.execute_no_params(&format!(
"ALTER TABLE {} ADD COLUMN {};",
table, latest_column.into_sqlite()
)).unwrap();
warn!(target: "migration", "Added column {} to table {} without migrating data.", latest_column.name(), table);
}
}
let columns = connection.get_all_columns(table).unwrap();
for latest_column in latest_schema.tables.get(&table.clone()).unwrap().iter() {
let column = columns.iter().find(|c| c.name() == latest_column.name()).unwrap();
// The column is in the latest schema, compare the types.
// TODO: Default value
if column.ty != latest_column.ty || !column.same_flags(latest_column) {
// The column type is not the same, use alter table to change it.
// safety note: this is safe because the column name is checked against the latest schema.
replace_table_full(connection, table, latest_schema.tables.get(&table.clone()).unwrap());
warn!(target: "migration", "Migrated whole table while altering column {} in table {} from '{}' to '{}'.", column.name(), table, column.ty.into_sqlite(), latest_column.ty.into_sqlite());
break; // The table has been replaced, no need to continue.
}
}
} else {
// The table is not in the latest schema, drop it.
connection.execute_no_params(&format!("DROP TABLE IF EXISTS {}", table)).unwrap();
warn!(target: "migration", "Dropped table {}.", table);
}
}
// Create any tables that are in the latest schema but not in the database.
for (table, columns) in latest_schema.tables.iter() {
if !tables.contains(table) {
// The table is not in the database, create it.
let mut sql = format!("CREATE TABLE {} (", table);
for column in columns.iter() {
sql.push_str(&format!("{},", column.into_sqlite()));
}
sql.pop();
sql.push(')');
connection.execute_no_params(&sql).unwrap();
debug!(target: "query_internal", "Created table using: {}", sql);
warn!(target: "migration", "Created table {} as it has not been found in current database.", table);
}
}
}
}
fn replace_table_full(connection: &Connection, table: &str, columns: &[Column]) {
let mut sql = format!("CREATE TABLE temp_{}_new (", table);
for column in columns.iter() {
sql.push_str(&format!("{},", column.into_sqlite()));
}
sql.pop();
sql.push(')');
connection.execute_no_params(&sql).unwrap();
// Copy the data from the old table to the new table.
connection.execute_no_params(&format!(
"INSERT INTO temp_{}_new SELECT * FROM {};",
table, table
)).unwrap();
// Drop the old table.
connection.execute_no_params(&format!("DROP TABLE IF EXISTS {}", table)).unwrap();
// Rename the new table to the old table.
connection.execute_no_params(&format!(
"ALTER TABLE temp_{}_new RENAME TO {};",
table, table
)).unwrap();
}
#[derive(Default)]
pub struct DbSchema<'a> {
// Name -> Fields
pub tables: HashMap<String, &'a [Column<'a>]>
}
impl DbSchema<'_> {
pub fn new() -> Self {
Self {
tables: HashMap::new()
}
}
pub fn add_table<M: Model>(&mut self) {
self.tables.insert(M::table_name().to_string(), M::columns());
}
}