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}