migrations/
lib.rs

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}