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}