#
cargo/util/
sqlite.rs

    1//! Utilities to help with working with sqlite.
    2
    3use crate::util::interning::InternedString;
    4use crate::CargoResult;
    5use rusqlite::types::{FromSql, FromSqlError, ToSql, ToSqlOutput};
    6use rusqlite::{Connection, TransactionBehavior};
    7
    8impl FromSql for InternedString {
    9    fn column_result(value: rusqlite::types::ValueRef<'_>) -> Result<Self, FromSqlError> {
    10        value.as_str().map(InternedString::new)
    11    }
    12}
    13
    14impl ToSql for InternedString {
    15    fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> {
    16        Ok(ToSqlOutput::from(self.as_str()))
    17    }
    18}
    19
    20/// A function or closure representing a database migration.
    21///
    22/// Migrations support evolving the schema and contents of the database across
    23/// new versions of cargo. The [`migrate`] function should be called
    24/// immediately after opening a connection to a database in order to configure
    25/// the schema. Whether or not a migration has been done is tracked by the
    26/// `pragma_user_version` value in the database. Typically you include the
    27/// initial `CREATE TABLE` statements in the initial list, but as time goes on
    28/// you can add new tables or `ALTER TABLE` statements. The migration code
    29/// will only execute statements that haven't previously been run.
    30///
    31/// Important things to note about how you define migrations:
    32///
    33/// * Never remove a migration entry from the list. Migrations are tracked by
    34///   the index number in the list.
    35/// * Never perform any schema modifications that would be backwards
    36///   incompatible. For example, don't drop tables or columns.
    37///
    38/// The [`basic_migration`] function is a convenience function for specifying
    39/// migrations that are simple SQL statements. If you need to do something
    40/// more complex, then you can specify a closure that takes a [`Connection`]
    41/// and does whatever is needed.
    42///
    43/// For example:
    44///
    45/// ```rust
    46/// # use cargo::util::sqlite::*;
    47/// # use rusqlite::Connection;
    48/// # let mut conn = Connection::open_in_memory()?;
    49/// # fn generate_name() -> String { "example".to_string() };
    50/// migrate(
    51///     &mut conn,
    52///     &[
    53///         basic_migration(
    54///             "CREATE TABLE foo (
    55///                 id INTEGER PRIMARY KEY AUTOINCREMENT,
    56///                 name STRING NOT NULL
    57///             )",
    58///         ),
    59///         Box::new(|conn| {
    60///             conn.execute("INSERT INTO foo (name) VALUES (?1)", [generate_name()])?;
    61///             Ok(())
    62///         }),
    63///         basic_migration("ALTER TABLE foo ADD COLUMN size INTEGER"),
    64///     ],
    65/// )?;
    66/// # Ok::<(), anyhow::Error>(())
    67/// ```
    68pub type Migration = Box<dyn Fn(&Connection) -> CargoResult<()>>;
    69
    70/// A basic migration that is a single static SQL statement.
    71///
    72/// See [`Migration`] for more information.
    73pub fn basic_migration(stmt: &'static str) -> Migration {
    74    Box::new(|conn| {
    75        conn.execute(stmt, [])?;
    76        Ok(())
    77    })
    78}
    79
    80/// Perform one-time SQL migrations.
    81///
    82/// See [`Migration`] for more information.
    83pub fn migrate(conn: &mut Connection, migrations: &[Migration]) -> CargoResult<()> {
    84    // EXCLUSIVE ensures that it starts with an exclusive write lock. No other
    85    // readers will be allowed. This generally shouldn't be needed if there is
    86    // a file lock, but might be helpful in cases where cargo's `FileLock`
    87    // failed.
    88    let tx = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
    89    let user_version = tx.query_row("SELECT user_version FROM pragma_user_version", [], |row| {
    90        row.get(0)
    91    })?;
    92    if user_version < migrations.len() {
    93        for migration in &migrations[user_version..] {
    94            migration(&tx)?;
    95        }
    96        tx.pragma_update(None, "user_version", &migrations.len())?;
    97    }
    98    tx.commit()?;
    99    Ok(())
    100}
    101
    102#[cfg(test)]
    103mod tests {
    104    use super::*;
    105
    106    #[test]
    107    fn migrate_twice() -> CargoResult<()> {
    108        // Check that a second migration will apply.
    109        let mut conn = Connection::open_in_memory()?;
    110        let mut migrations = vec![basic_migration("CREATE TABLE foo (a, b, c)")];
    111        migrate(&mut conn, &migrations)?;
    112        conn.execute("INSERT INTO foo VALUES (1,2,3)", [])?;
    113        migrations.push(basic_migration("ALTER TABLE foo ADD COLUMN d"));
    114        migrate(&mut conn, &migrations)?;
    115        conn.execute("INSERT INTO foo VALUES (1,2,3,4)", [])?;
    116        Ok(())
    117    }
    118}