aboutsummaryrefslogtreecommitdiff
path: root/src/cli/make_migrations.rs
blob: 83e760cc707878a513b4aa8163f0a743db19f63a (plain)
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
        }
    }
}