Skip to main content

schemerz_rusqlite/
lib.rs

1//! An adapter enabling use of the schemerz schema migration library with
2//! SQLite3.
3//!
4//! # Examples:
5//!
6//! ```rust
7//! extern crate rusqlite;
8//! #[macro_use]
9//! extern crate schemerz;
10//! extern crate schemerz_rusqlite;
11//! extern crate uuid;
12//!
13//! use std::collections::HashSet;
14//!
15//! use rusqlite::{params, Connection, Transaction, Error as RusqliteError};
16//! use schemerz::{Migration, Migrator};
17//! use schemerz_rusqlite::{RusqliteAdapter, RusqliteAdapterError, RusqliteMigration};
18//! use uuid::uuid;
19//!
20//! struct MyExampleMigration;
21//! migration!(
22//!     MyExampleMigration,
23//!     uuid!("4885e8ab-dafa-4d76-a565-2dee8b04ef60"),
24//!     [],
25//!     "An example migration without dependencies.");
26//!
27//! impl RusqliteMigration for MyExampleMigration {
28//!     type Error = RusqliteError;
29//!
30//!     fn up(&self, transaction: &Transaction) -> Result<(), RusqliteAdapterError> {
31//!         transaction.execute("CREATE TABLE my_example (id integer PRIMARY KEY);", params![])?;
32//!         Ok(())
33//!     }
34//!
35//!     fn down(&self, transaction: &Transaction) -> Result<(), RusqliteAdapterError> {
36//!         transaction.execute("DROP TABLE my_example;", params![])?;
37//!         Ok(())
38//!     }
39//! }
40//!
41//! fn main() {
42//!     let mut conn = Connection::open_in_memory().unwrap();
43//!     let adapter = RusqliteAdapter::new(&mut conn, None);
44//!
45//!     let mut migrator = Migrator::new(adapter);
46//!
47//!     let migration = Box::new(MyExampleMigration {});
48//!     migrator.register(migration);
49//!     migrator.up(None);
50//! }
51//! ```
52#![warn(clippy::all)]
53#![forbid(unsafe_code)]
54
55use std::collections::HashSet;
56use std::error::Error;
57use std::marker::{PhantomData, Send, Sync};
58
59use rusqlite::{params, Connection, Error as RusqliteError, Transaction};
60use uuid::Uuid;
61
62use schemerz::{Adapter, Migration};
63
64/// SQlite-specific trait for schema migrations.
65pub trait RusqliteMigration: Migration<Uuid> {
66    type Error: From<RusqliteError>;
67
68    /// Apply a migration to the database using a transaction.
69    fn up(&self, _transaction: &Transaction<'_>) -> Result<(), Self::Error> {
70        Ok(())
71    }
72
73    /// Revert a migration to the database using a transaction.
74    fn down(&self, _transaction: &Transaction<'_>) -> Result<(), Self::Error> {
75        Ok(())
76    }
77}
78
79pub type RusqliteAdapterError = RusqliteError;
80
81struct WrappedUuid(Uuid);
82
83impl rusqlite::types::FromSql for WrappedUuid {
84    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
85        Ok(WrappedUuid(Uuid::from_slice(value.as_blob()?).map_err(
86            |e| rusqlite::types::FromSqlError::Other(Box::new(e)),
87        )?))
88    }
89}
90
91/// Adapter between schemerz and SQLite.
92pub struct RusqliteAdapter<'a, E> {
93    conn: &'a mut Connection,
94    migration_metadata_table: String,
95    _err: PhantomData<E>,
96}
97
98impl<'a, E> RusqliteAdapter<'a, E> {
99    /// Construct a SQLite schemerz adapter.
100    ///
101    /// `table_name` specifies the name of the table that schemerz will use
102    /// for storing metadata about applied migrations. If `None`, a default
103    /// will be used.
104    ///
105    /// ```rust
106    /// # extern crate rusqlite;
107    /// # use rusqlite::{Error as RusqliteError};
108    /// #
109    /// # fn main() {
110    /// let mut conn = rusqlite::Connection::open_in_memory().unwrap();
111    /// let adapter: schemerz_rusqlite::RusqliteAdapter<RusqliteError> = schemerz_rusqlite::RusqliteAdapter::new(&mut conn, None);
112    /// # }
113    /// ```
114    pub fn new(conn: &'a mut Connection, table_name: Option<String>) -> RusqliteAdapter<'a, E> {
115        RusqliteAdapter {
116            conn,
117            migration_metadata_table: table_name.unwrap_or_else(|| "_schemerz".into()),
118            _err: PhantomData,
119        }
120    }
121
122    /// Initialize the schemerz metadata schema. This must be called before
123    /// using `Migrator` with this adapter. This is safe to call multiple times.
124    pub fn init(&self) -> Result<(), RusqliteError> {
125        self.conn.execute(
126            &format!(
127                r#"
128                    CREATE TABLE IF NOT EXISTS {} (
129                        id blob PRIMARY KEY
130                    )
131                "#,
132                self.migration_metadata_table
133            ),
134            params![],
135        )?;
136        Ok(())
137    }
138}
139
140impl<'a, E> Adapter<Uuid> for RusqliteAdapter<'a, E>
141where
142    E: From<RusqliteError> + Sync + Send + Error + 'static,
143{
144    type MigrationType = Box<dyn RusqliteMigration<Error = E>>;
145
146    type Error = E;
147
148    fn applied_migrations(&mut self) -> Result<HashSet<Uuid>, Self::Error> {
149        let mut stmt = self.conn.prepare(&format!(
150            "SELECT id FROM {};",
151            self.migration_metadata_table
152        ))?;
153        // TODO: have to do this rather than `collect` because Rusqlite has an
154        // interface that goes against map conventions.
155        let rows = stmt.query_map(params![], |row| row.get::<_, WrappedUuid>(0))?;
156        let mut ids = HashSet::new();
157        for row in rows {
158            ids.insert(row?.0);
159        }
160        Ok(ids)
161    }
162
163    fn apply_migration(&mut self, migration: &Self::MigrationType) -> Result<(), Self::Error> {
164        let trans = self.conn.transaction()?;
165        migration.up(&trans)?;
166        let uuid = migration.id();
167        let uuid_bytes = &uuid.as_bytes()[..];
168        trans.execute(
169            &format!(
170                "INSERT INTO {} (id) VALUES (?1);",
171                self.migration_metadata_table
172            ),
173            [&uuid_bytes],
174        )?;
175        trans.commit().map_err(|e| e.into())
176    }
177
178    fn revert_migration(&mut self, migration: &Self::MigrationType) -> Result<(), Self::Error> {
179        let trans = self.conn.transaction()?;
180        migration.down(&trans)?;
181        let uuid = migration.id();
182        let uuid_bytes = &uuid.as_bytes()[..];
183        trans.execute(
184            &format!(
185                "DELETE FROM {} WHERE id = ?1;",
186                self.migration_metadata_table
187            ),
188            [&uuid_bytes],
189        )?;
190        trans.commit().map_err(|e| e.into())
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use rusqlite::Error as RusqliteError;
198    use schemerz::test_schemerz_adapter;
199    use schemerz::testing::*;
200
201    impl RusqliteMigration for TestMigration<Uuid> {
202        type Error = RusqliteError;
203    }
204
205    impl<'a> TestAdapter<Uuid> for RusqliteAdapter<'a, RusqliteError> {
206        fn mock(id: Uuid, dependencies: HashSet<Uuid>) -> Self::MigrationType {
207            Box::new(TestMigration::new(id, dependencies))
208        }
209    }
210
211    fn build_test_connection() -> Connection {
212        Connection::open_in_memory().unwrap()
213    }
214
215    fn build_test_adapter(conn: &mut Connection) -> RusqliteAdapter<'_, RusqliteError> {
216        let adapter = RusqliteAdapter::new(conn, None);
217        adapter.init().unwrap();
218        adapter
219    }
220
221    fn uuid_iter() -> impl Iterator<Item = Uuid> {
222        (0..).map(|v| Uuid::from_fields(v as u32, v, v, &[0; 8]))
223    }
224
225    test_schemerz_adapter!(
226        let mut conn = build_test_connection(),
227        build_test_adapter(&mut conn),
228        uuid_iter(),
229    );
230}