cargo/util/sqlite.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
//! Utilities to help with working with sqlite.
use crate::util::interning::InternedString;
use crate::CargoResult;
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::new)
}
}
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(())
}
}