1pub mod db;
2pub mod diff;
3
4use std::env::current_dir;
5use std::error::Error;
6use std::fs::{copy, create_dir, read_dir, read_to_string, File};
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use toml::Value;
11
12use ergol_core::{Element, Table};
13
14use crate::diff::{diff, Diff, State};
15
16pub fn order(tables: Vec<Table>) -> Vec<Table> {
18 let mut current: Vec<String> = vec![];
19 let mut output_tables = vec![];
20 let len = tables.len();
21
22 for _ in 0..len {
23 for table in &tables {
24 if !current.contains(&table.name)
26 && table.dependencies().iter().all(|x| current.contains(x))
27 {
28 current.push(table.name.clone());
29 output_tables.push(table.clone());
30 }
31 }
32 }
33
34 if output_tables.len() != len {
35 tables
36 } else {
37 output_tables
38 }
39}
40
41pub fn find_cargo_toml() -> Option<PathBuf> {
43 let mut current = current_dir().ok()?;
44
45 loop {
46 if current.join("Cargo.toml").is_file() {
47 return Some(current);
48 }
49
50 if !current.pop() {
51 return None;
52 }
53 }
54}
55
56pub fn last_saved_state<P: AsRef<Path>>(p: P) -> Result<(Option<u32>, State), Box<dyn Error>> {
58 let p = p.as_ref();
59 let mut current = 0;
60
61 loop {
62 if !p.join(format!("{}", current)).is_dir() {
63 if current == 0 {
64 return Ok((None, (vec![], vec![])));
66 } else {
67 return state_from_dir(p.join(format!("{}", current - 1)))
68 .map(|x| (Some(current - 1), x));
69 }
70 }
71
72 current += 1;
73 }
74}
75
76pub fn state_from_dir<P: AsRef<Path>>(path: P) -> Result<State, Box<dyn Error>> {
78 let mut tables = vec![];
79 let mut enums = vec![];
80
81 for file in read_dir(path.as_ref())? {
82 let path = file?.path();
83 if path.extension().and_then(|x| x.to_str()) == Some("json") {
84 let content = read_to_string(path)?;
85 let elements: Vec<Element> = serde_json::from_str(&content)?;
86 for element in elements {
87 match element {
88 Element::Enum(e) => enums.push(e),
89 Element::Table(t) => tables.push(t),
90 }
91 }
92 }
93 }
94 Ok((enums, order(tables)))
95}
96
97pub fn find_db_url<P: AsRef<Path>>(path: P) -> Option<String> {
99 let path = path.as_ref();
100
101 let path = if path.join("Ergol.toml").is_file() {
102 path.join("Ergol.toml")
103 } else if path.join("Rocket.toml").is_file() {
104 path.join("Rocket.toml")
105 } else {
106 return None;
107 };
108
109 let content = read_to_string(path).ok()?;
110 let value = content.parse::<Value>().ok()?;
111
112 let url = value
113 .as_table()?
114 .get("default")?
115 .as_table()?
116 .get("databases")?
117 .as_table()?
118 .get("database")?
119 .as_table()?
120 .get("url")?
121 .as_str()?;
122
123 Some(url.into())
124}
125
126pub async fn migrate<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
128 let path = path.as_ref();
129 let db_url = find_db_url(&path).unwrap();
130
131 let (db, connection) = tokio_postgres::connect(&db_url, tokio_postgres::NoTls).await?;
132
133 tokio::spawn(async move {
134 if let Err(e) = connection.await {
135 eprintln!("connection error: {}", e);
136 }
137 });
138
139 let current = db::current_migration(&db).await?;
140
141 let mut current = match current {
142 Some(i) => i + 1,
143 None => {
144 db::create_current_migration(&db).await?;
145 0
146 }
147 };
148
149 loop {
151 let path = path.join(format!("migrations/{}/up.sql", current));
152
153 if !path.is_file() {
154 break;
155 }
156
157 let up = read_to_string(path)?;
158 println!("{}", up);
159
160 db.simple_query(&up as &str).await?;
161 db::set_migration(current, &db).await?;
162
163 current += 1;
164 }
165
166 Ok(())
167}
168
169pub fn current_diff<P: AsRef<Path>>(path: P) -> Result<Diff, Box<dyn Error>> {
171 let path = path.as_ref();
172
173 let last = last_saved_state(path.join("migrations"))?;
174 let current = state_from_dir(path.join("migrations/current"))?;
175
176 Ok(diff(last.1, current))
177}
178
179pub async fn delete<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
181 let path = path.as_ref();
182 let db_url = find_db_url(&path).unwrap();
183
184 let (db, connection) = tokio_postgres::connect(&db_url, tokio_postgres::NoTls).await?;
185
186 tokio::spawn(async move {
187 if let Err(e) = connection.await {
188 eprintln!("connection error: {}", e);
189 }
190 });
191
192 db::clear(&db).await?;
193
194 Ok(())
195}
196
197pub fn save<P: AsRef<Path>>(p: P) -> Result<(), Box<dyn Error>> {
199 let p = p.as_ref();
200 let (last_index, last_state) = last_saved_state(p)?;
201 let current_state = state_from_dir(p.join("current"))?;
202 let current_index = match last_index {
203 None => 0,
204 Some(i) => i + 1,
205 };
206
207 let save_dir = p.join(format!("{}", current_index));
208 create_dir(&save_dir)?;
209 for f in read_dir(p.join("current"))? {
210 let path = f?.path();
211 copy(&path, &save_dir.join(path.file_name().unwrap()))?;
212 }
213
214 let diff = diff(last_state, current_state);
215 let mut file = File::create(save_dir.join("up.sql"))?;
216 file.write_all(diff.hint().as_bytes())?;
217
218 let mut file = File::create(save_dir.join("down.sql"))?;
219 file.write_all(diff.hint_revert().as_bytes())?;
220
221 Ok(())
222}
223
224pub async fn reset<P: AsRef<Path>>(p: P) -> Result<(), Box<dyn Error>> {
226 let p = p.as_ref();
227 delete(p).await?;
228 let (enums, tables) = state_from_dir(p.join("migrations/current"))?;
229
230 let db_url = find_db_url(p).unwrap();
231 let (db, connection) = tokio_postgres::connect(&db_url, tokio_postgres::NoTls).await?;
232
233 tokio::spawn(async move {
234 if let Err(e) = connection.await {
235 eprintln!("connection error: {}", e);
236 }
237 });
238
239 for e in enums {
240 db.query(&Element::Enum(e).create() as &str, &[]).await?;
241 }
242
243 for t in tables {
244 db.query(&Element::Table(t).create() as &str, &[]).await?;
245 }
246
247 Ok(())
248}