Skip to content

Commit a788bea

Browse files
committed
Add a module with sqlite utilities.
This adds a module as a home for general sqlite support. Initially this contains a very simple schema migration support system.
1 parent 3399cfa commit a788bea

File tree

4 files changed

+178
-0
lines changed

4 files changed

+178
-0
lines changed

Cargo.lock

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ pretty_assertions = "1.4.0"
7171
proptest = "1.2.0"
7272
pulldown-cmark = { version = "0.9.3", default-features = false }
7373
rand = "0.8.5"
74+
rusqlite = { version = "0.29.0", features = ["bundled"] }
7475
rustfix = "0.6.1"
7576
same-file = "1.0.6"
7677
security-framework = "2.9.2"
@@ -160,6 +161,7 @@ pasetors.workspace = true
160161
pathdiff.workspace = true
161162
pulldown-cmark.workspace = true
162163
rand.workspace = true
164+
rusqlite.workspace = true
163165
rustfix.workspace = true
164166
semver.workspace = true
165167
serde = { workspace = true, features = ["derive"] }

src/cargo/util/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ mod queue;
6161
pub mod restricted_names;
6262
pub mod rustc;
6363
mod semver_ext;
64+
pub mod sqlite;
6465
pub mod to_semver;
6566
pub mod toml;
6667
pub mod toml_mut;

src/cargo/util/sqlite.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! Utilities to help with working with sqlite.
2+
3+
use crate::util::interning::InternedString;
4+
use crate::CargoResult;
5+
use rusqlite::types::{FromSql, FromSqlError, ToSql, ToSqlOutput};
6+
use rusqlite::{Connection, TransactionBehavior};
7+
8+
impl FromSql for InternedString {
9+
fn column_result(value: rusqlite::types::ValueRef<'_>) -> Result<Self, FromSqlError> {
10+
value.as_str().map(InternedString::new)
11+
}
12+
}
13+
14+
impl 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+
/// ```
68+
pub 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.
73+
pub 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.
83+
pub 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)]
103+
mod 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+
}

0 commit comments

Comments
 (0)