| //! Utilities to help with working with sqlite. | |
| use crate::CargoResult; | |
| use crate::util::interning::InternedString; | |
| use rusqlite::types::{FromSql, FromSqlError, ToSql, ToSqlOutput}; | |
| use rusqlite::{Connection, TransactionBehavior}; | |
| impl FromSql for InternedString { | |
| fn column_result(value: rusqlite::types::ValueRef<'_>) -> Result<Self, FromSqlError> { | |
| value.as_str().map(InternedString::from) | |
| } | |
| } | |
| impl ToSql for InternedString { | |
| fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> { | |
| Ok(ToSqlOutput::from(self.as_str())) | |
| } | |
| } | |
| /// A function or closure representing a database migration. | |
| /// | |
| /// Migrations support evolving the schema and contents of the database across | |
| /// new versions of cargo. The [`migrate`] function should be called | |
| /// immediately after opening a connection to a database in order to configure | |
| /// the schema. Whether or not a migration has been done is tracked by the | |
| /// `pragma_user_version` value in the database. Typically you include the | |
| /// initial `CREATE TABLE` statements in the initial list, but as time goes on | |
| /// you can add new tables or `ALTER TABLE` statements. The migration code | |
| /// will only execute statements that haven't previously been run. | |
| /// | |
| /// Important things to note about how you define migrations: | |
| /// | |
| /// * Never remove a migration entry from the list. Migrations are tracked by | |
| /// the index number in the list. | |
| /// * Never perform any schema modifications that would be backwards | |
| /// incompatible. For example, don't drop tables or columns. | |
| /// | |
| /// The [`basic_migration`] function is a convenience function for specifying | |
| /// migrations that are simple SQL statements. If you need to do something | |
| /// more complex, then you can specify a closure that takes a [`Connection`] | |
| /// and does whatever is needed. | |
| /// | |
| /// For example: | |
| /// | |
| /// ```rust | |
| /// # use cargo::util::sqlite::*; | |
| /// # use rusqlite::Connection; | |
| /// # let mut conn = Connection::open_in_memory()?; | |
| /// # fn generate_name() -> String { "example".to_string() }; | |
| /// migrate( | |
| /// &mut conn, | |
| /// &[ | |
| /// basic_migration( | |
| /// "CREATE TABLE foo ( | |
| /// id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| /// name STRING NOT NULL | |
| /// )", | |
| /// ), | |
| /// Box::new(|conn| { | |
| /// conn.execute("INSERT INTO foo (name) VALUES (?1)", [generate_name()])?; | |
| /// Ok(()) | |
| /// }), | |
| /// basic_migration("ALTER TABLE foo ADD COLUMN size INTEGER"), | |
| /// ], | |
| /// )?; | |
| /// # Ok::<(), anyhow::Error>(()) | |
| /// ``` | |
| pub type Migration = Box<dyn Fn(&Connection) -> CargoResult<()>>; | |
| /// A basic migration that is a single static SQL statement. | |
| /// | |
| /// See [`Migration`] for more information. | |
| pub fn basic_migration(stmt: &'static str) -> Migration { | |
| Box::new(|conn| { | |
| conn.execute(stmt, [])?; | |
| Ok(()) | |
| }) | |
| } | |
| /// Perform one-time SQL migrations. | |
| /// | |
| /// See [`Migration`] for more information. | |
| pub fn migrate(conn: &mut Connection, migrations: &[Migration]) -> CargoResult<()> { | |
| // EXCLUSIVE ensures that it starts with an exclusive write lock. No other | |
| // readers will be allowed. This generally shouldn't be needed if there is | |
| // a file lock, but might be helpful in cases where cargo's `FileLock` | |
| // failed. | |
| let tx = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; | |
| let user_version = tx.query_row("SELECT user_version FROM pragma_user_version", [], |row| { | |
| row.get(0) | |
| })?; | |
| if user_version < migrations.len() { | |
| for migration in &migrations[user_version..] { | |
| migration(&tx)?; | |
| } | |
| tx.pragma_update(None, "user_version", &migrations.len())?; | |
| } | |
| tx.commit()?; | |
| Ok(()) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| #[test] | |
| fn migrate_twice() -> CargoResult<()> { | |
| // Check that a second migration will apply. | |
| let mut conn = Connection::open_in_memory()?; | |
| let mut migrations = vec![basic_migration("CREATE TABLE foo (a, b, c)")]; | |
| migrate(&mut conn, &migrations)?; | |
| conn.execute("INSERT INTO foo VALUES (1,2,3)", [])?; | |
| migrations.push(basic_migration("ALTER TABLE foo ADD COLUMN d")); | |
| migrate(&mut conn, &migrations)?; | |
| conn.execute("INSERT INTO foo VALUES (1,2,3,4)", [])?; | |
| Ok(()) | |
| } | |
| } |