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.
This commit is contained in:
Eric Huss 2023-09-06 21:22:23 -07:00
parent 9588fb6dda
commit 34fdf5fb03
4 changed files with 189 additions and 0 deletions

68
Cargo.lock generated
View File

@ -8,6 +8,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "1.0.2"
@ -17,6 +28,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "anes"
version = "0.1.6"
@ -270,6 +287,7 @@ dependencies = [
"pathdiff",
"pulldown-cmark",
"rand",
"rusqlite",
"rustfix",
"same-file",
"semver",
@ -886,6 +904,18 @@ dependencies = [
"serde_json",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "faster-hex"
version = "0.8.1"
@ -1806,6 +1836,19 @@ name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashlink"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown",
]
[[package]]
name = "hermit-abi"
@ -2060,6 +2103,17 @@ dependencies = [
"libc",
]
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libssh2-sys"
version = "0.3.0"
@ -2799,6 +2853,20 @@ dependencies = [
"subtle",
]
[[package]]
name = "rusqlite"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags 2.4.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"

View File

@ -73,6 +73,7 @@ pretty_assertions = "1.4.0"
proptest = "1.3.1"
pulldown-cmark = { version = "0.9.3", default-features = false }
rand = "0.8.5"
rusqlite = { version = "0.29.0", features = ["bundled"] }
rustfix = "0.6.1"
same-file = "1.0.6"
security-framework = "2.9.2"
@ -162,6 +163,7 @@ pasetors.workspace = true
pathdiff.workspace = true
pulldown-cmark.workspace = true
rand.workspace = true
rusqlite.workspace = true
rustfix.workspace = true
semver.workspace = true
serde = { workspace = true, features = ["derive"] }

View File

@ -62,6 +62,7 @@ mod queue;
pub mod restricted_names;
pub mod rustc;
mod semver_ext;
pub mod sqlite;
pub mod style;
pub mod toml;
pub mod toml_mut;

118
src/cargo/util/sqlite.rs Normal file
View File

@ -0,0 +1,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(())
}
}