#
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(())
        }
    }