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
|
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use quote::{quote, ToTokens};
use structopt::StructOpt;
use crate::{Settings, UrlMap, db::backend::Connectable};
use crate::db::migration::{Migration, DatabaseChange, CreateModelOption};
use crate::db::models::{Field, ModelMeta};
#[derive(StructOpt)]
/// Generate migrations
pub struct MakeMigrations {
}
#[derive(Debug)]
struct AppTablesState {
db: HashMap<&'static str, TableState>,
}
#[derive(Debug)]
struct TableState {
fields: Vec<Field>,
}
impl AppTablesState {
fn changes_to(self, dest: AppTablesState) -> Vec<impl ToTokens> {
let mut result = vec![];
for table in dest.db.keys() {
if !self.db.contains_key(table) {
let fields = &dest.db[table].fields.iter().map(|field| {
match field {
Field::CharField { name, max_length: Some(max_length) } => quote! { Field::CharField { name: #name, max_length: Some(#max_length) } },
Field::CharField { name, max_length: None } => quote! { Field::CharField { name: #name, max_length: None } },
Field::DateTimeField { name } => quote! { Field::DateTimeField { name: #name } },
Field::IntField { name } => quote! { Field::IntField { name: #name } },
}
}).collect::<Vec<_>>();
result.push(quote! {
DatabaseChange::CreateModel {
name: #table,
fields: &[
#(#fields),*
],
options: &[],
}
});
}
}
result
}
}
impl From<&[ModelMeta]> for AppTablesState {
fn from(models: &[ModelMeta]) -> Self {
let mut db = HashMap::new();
for model in models {
db.insert(model.name, TableState { fields: model.fields.into() });
}
Self { db }
}
}
impl From<&[Migration]> for AppTablesState {
fn from(migrations: &[Migration]) -> Self {
let mut db = HashMap::new();
for migration in migrations {
for change in migration.changes {
match change {
DatabaseChange::CreateModel { name, fields, options } => {
if db.contains_key(name) {
if options.contains(&CreateModelOption::IfNotExist) {
continue;
} else {
panic!("double-created table {}", name);
}
}
db.insert(*name, TableState { fields: (*fields).into() });
}
}
}
}
Self { db }
}
}
impl MakeMigrations {
pub fn execute(self, _urls: UrlMap, settings: Settings<impl Connectable>) {
for app in settings.installed_apps {
let expected_table_state = AppTablesState::from(app.models);
let actual_table_state = AppTablesState::from(app.migrations);
let next_id = app.migrations.iter().map(|m| m.id).max().map_or(1, |x| x + 1);
let name = "auto"; // TODO names
let changes = actual_table_state.changes_to(expected_table_state);
if changes.is_empty() { continue; }
let migration = quote! {
Migration {
id: #next_id,
name: #name,
prereqs: &[], // TODO
changes: &[ #(#changes),* ],
}
};
let file = quote! {
use tosin::db::migration::Migration;
pub const MIGRATION: Migration = #migration;
};
let file_name = format!("m_{:04}_{}.rs", next_id, name);
let file_text = file.into_token_stream().to_string();
let app_folder = app.name.split("::")
.skip(1)
.collect::<PathBuf>();
// TODO don't explode if running in a weird place
let file_path = PathBuf::from("src")
.join(app_folder)
.join("migrations")
.join(file_name);
println!("Saving migration to {}...", file_path.display());
fs::write(file_path, file_text).unwrap();
// TODO cargo fmt
}
}
}
|