1use anyhow::Result;
2use log::{error, info, warn};
3
4pub use changeset_store::{Changeset, ChangesetStore, ChangesetStores};
5pub use identifier::Identifier;
6pub use migration_store::{FileActions, MigrationStore, MigrationStores};
7
8#[cfg(feature = "sql")]
9pub use changeset_store::{SqlChangeset, SqlChangesetStore};
10
11#[cfg(feature = "sql")]
12pub use custom::Dialect;
13
14mod changeset_store;
15mod custom;
16mod identifier;
17mod migration_store;
18
19pub struct Migrations {
20 store: Box<dyn MigrationStore>,
21 changes: Box<dyn ChangesetStore>,
22}
23
24impl Migrations {
25 pub fn new(store: Box<dyn MigrationStore>, changes: Box<dyn ChangesetStore>) -> Migrations {
26 Migrations { store, changes }
27 }
28
29 pub fn setup(&self) -> Result<()> {
30 if !self.store.is_ready()? {
31 self.store.setup()?;
32 }
33 Ok(())
34 }
35
36 pub fn is_applied(&self, identifier: &Identifier) -> bool {
37 self.store.is_applied(identifier)
38 }
39
40 pub fn create_changeset(&self, name: String) -> Result<Box<dyn Changeset + '_>> {
41 self.changes.create_changeset(name)
42 }
43
44 pub fn reset(&mut self) -> Result<()> {
45 for changeset in self.changes.get_changesets()? {
46 if self.store.is_applied(&changeset.identifier()) {
47 self.store.rollback(&changeset)?;
48 }
49 }
50 Ok(())
51 }
52
53 pub fn rollback(&mut self) -> Result<()> {
54 let changesets = self.changes.get_changesets()?;
55 let last = changesets
56 .iter()
57 .filter(|c| self.store.is_applied(&c.identifier()))
58 .max_by(|a, b| a.identifier().value().cmp(b.identifier().value()));
59
60 if last.is_none() {
61 warn!("No changeset_store to rollback");
62 return Ok(());
63 }
64 let last = last.unwrap();
65 match self.store.rollback(last) {
66 Ok(_) => {
67 info!("Rolled back {}", last.identifier());
68 Ok(())
69 }
70 Err(e) => {
71 error!("Failed to rollback {}", last.identifier());
72 Err(e)
73 }
74 }
75 }
76
77 pub fn migrate(&mut self) -> Result<()> {
78 for change in &self.changes.get_changesets()? {
79 if !self.store.is_applied(&change.identifier()) {
80 match self.store.apply(change) {
81 Ok(_) => {
82 info!("Applied {}", change.identifier())
83 }
84 Err(e) => {
85 error!("Failed to apply {}: {:?}", change.identifier(), e);
86 return Err(e);
87 }
88 }
89 }
90 }
91 Ok(())
92 }
93
94 #[cfg(feature = "libsql")]
95 pub fn libsql(
96 connection: libsql::Connection,
97 migration_directory: std::path::PathBuf,
98 ) -> Migrations {
99 let store = MigrationStores::libsql(connection.clone());
100 let changes = ChangesetStores::sql_store(migration_directory);
101 Self::new(store, changes)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::changeset_store::ChangesetStores;
109 use crate::migration_store::MigrationStores;
110 use std::env;
111 use std::fs::create_dir;
112 use std::path::{Path, PathBuf};
113
114 pub struct FileChangeset {
115 path: PathBuf,
116 }
117
118 impl FileChangeset {
119 pub fn new(path: PathBuf) -> Box<dyn Changeset> {
120 Box::new(FileChangeset { path })
121 }
122 }
123
124 impl Changeset for FileChangeset {
125 fn identifier(&self) -> Identifier {
126 Identifier::from_string(self.path.file_name().unwrap().to_str().unwrap())
127 }
128
129 fn apply(&self, _store: &dyn MigrationStore) -> anyhow::Result<()> {
130 Ok(())
131 }
132
133 fn rollback(&self, _store: &dyn MigrationStore) -> anyhow::Result<()> {
134 Ok(())
135 }
136
137 fn duplicate(&self) -> Box<dyn Changeset> {
138 FileChangeset::new(self.path.clone())
139 }
140 }
141
142 fn empty_changeset(identifier: Identifier) -> Box<dyn Changeset> {
143 let raw = identifier.value();
144 let path = Path::new(raw);
145 if path.exists() {
146 warn!("Changeset path {} already exists", raw)
147 } else {
148 create_dir(path).unwrap();
149 }
150 FileChangeset::new(path.to_path_buf())
151 }
152
153 fn to_changeset(path: PathBuf) -> Box<dyn Changeset> {
154 FileChangeset::new(path.clone())
155 }
156
157 #[test]
158 fn migrations() {
159 let identifier = Identifier::from_string("20240620112020_test");
160 let store = MigrationStores::in_memory(vec![empty_changeset(identifier.clone())]);
161 let path = env::current_dir().expect("Failed to get current directory");
162 let changes = ChangesetStores::file_store(path, empty_changeset, to_changeset);
163 let mut migrations = Migrations { store, changes };
164 migrations.setup().unwrap();
165
166 assert_eq!(migrations.is_applied(&identifier), false);
167 migrations.migrate().unwrap();
168
169 assert_eq!(migrations.is_applied(&identifier), true);
170 }
171
172 #[test]
173 fn rollback() {
174 let identifier = Identifier::from_string("20240620112020_test");
175 let store = MigrationStores::in_memory(vec![empty_changeset(identifier.clone())]);
176 let path = env::current_dir().expect("Failed to get current directory");
177 let changes = ChangesetStores::file_store(path, empty_changeset, to_changeset);
178 let mut migrations = Migrations { store, changes };
179 migrations.setup().unwrap();
180 migrations.migrate().unwrap();
181 migrations.rollback().unwrap();
182
183 assert_eq!(migrations.is_applied(&identifier), false);
184 }
185}