From e21c289f8d21b802d2dc609233bc28cde65da224 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Sat, 22 Feb 2025 11:37:34 +0100 Subject: fix(yt/storage/migrate): Improve error reporting --- yt/src/storage/migrate/mod.rs | 123 +++++++++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 31 deletions(-) diff --git a/yt/src/storage/migrate/mod.rs b/yt/src/storage/migrate/mod.rs index ee43008..52f8c2b 100644 --- a/yt/src/storage/migrate/mod.rs +++ b/yt/src/storage/migrate/mod.rs @@ -25,6 +25,15 @@ pub enum DbVersion { } const CURRENT_VERSION: DbVersion = DbVersion::One; +async fn add_error_context( + function: impl Future>, + level: DbVersion, +) -> Result<()> { + function + .await + .with_context(|| format!("Format failed to migrate database to version: {level}")) +} + async fn set_db_version( tx: &mut Transaction<'_, Sqlite>, old_version: Option, @@ -83,40 +92,85 @@ impl DbVersion { /// /// Each update is atomic, so if this function fails you are still guaranteed to have a /// database at version `get_version`. + #[allow(clippy::too_many_lines)] async fn update(self, app: &App) -> Result<()> { match self { DbVersion::Empty => { - let mut tx = app.database.begin().await?; - debug!("Migrate: Empty -> Zero"); - - sqlx::raw_sql(include_str!("./sql/00_empty_to_zero.sql")) - .execute(&mut *tx) - .await?; - - set_db_version(&mut tx, None, DbVersion::Zero).await?; - - tx.commit().await?; + add_error_context( + async { + let mut tx = app + .database + .begin() + .await + .context("Failed to start transaction")?; + debug!("Migrate: Empty -> Zero"); + + sqlx::raw_sql(include_str!("./sql/00_empty_to_zero.sql")) + .execute(&mut *tx) + .await + .context("Failed to execute sql update script")?; + + set_db_version(&mut tx, None, DbVersion::Zero) + .await + .context("Failed to set new version")?; + + tx.commit().await.context("Failed to commit changes")?; + + // NOTE: This is needed, so that sqlite "sees" our changes to the table + // without having to reconnect. <2025-02-18> + query!("VACUUM") + .execute(&app.database) + .await + .context("Failed to vacuum database")?; + + Ok(()) + }, + DbVersion::One, + ) + .await?; Box::pin(Self::Zero.update(app)).await } DbVersion::Zero => { - let mut tx = app.database.begin().await?; - debug!("Migrate: Zero -> One"); - - sqlx::raw_sql(include_str!("./sql/01_zero_to_one.sql")) - .execute(&mut *tx) - .await?; + add_error_context( + async { + let mut tx = app + .database + .begin() + .await + .context("Failed to start transaction")?; + debug!("Migrate: Zero -> One"); + + sqlx::raw_sql(include_str!("./sql/01_zero_to_one.sql")) + .execute(&mut *tx) + .await + .context("Failed to execute the update sql script")?; + + set_db_version(&mut tx, Some(DbVersion::Zero), DbVersion::One) + .await + .context("Failed to set the new version")?; + + tx.commit() + .await + .context("Failed to commit the update transaction")?; + + // NOTE: This is needed, so that sqlite "sees" our changes to the table + // without having to reconnect. <2025-02-18> + query!("VACUUM") + .execute(&app.database) + .await + .context("Failed to vacuum database")?; + + Ok(()) + }, + DbVersion::Zero, + ) + .await?; - set_db_version(&mut tx, Some(DbVersion::Zero), DbVersion::One).await?; - - tx.commit().await?; Box::pin(Self::One.update(app)).await } - // This is the current_version DbVersion::One => { - debug!("Migrate: One -> Two"); - assert_eq!(self, CURRENT_VERSION); assert_eq!(self, get_version(app).await?); Ok(()) @@ -141,13 +195,8 @@ fn get_current_date() -> i64 { ) .expect("Time does not go backwards"); - debug!( - "Adding a date with timestamp: {}", - seconds_since_epoch.num_seconds() - ); - // All database dates should be after the UNIX_EPOCH (and thus positiv) - seconds_since_epoch.num_seconds() + seconds_since_epoch.num_milliseconds() } /// Return the current database version. @@ -155,11 +204,21 @@ fn get_current_date() -> i64 { /// # Panics /// Only if internal assertions fail. pub async fn get_version(app: &App) -> Result { + get_version_db(&app.database).await +} +/// Return the current database version. +/// +/// In contrast to the [`get_version`] function, this function does not +/// a fully instantiated [`App`], a database connection suffices. +/// +/// # Panics +/// Only if internal assertions fail. +pub async fn get_version_db(pool: &SqlitePool) -> Result { let version_table_exists = { let query = query!( "SELECT 1 as result FROM sqlite_master WHERE type = 'table' AND name = 'version'" ) - .fetch_optional(&app.database) + .fetch_optional(pool) .await?; if let Some(output) = query { assert_eq!(output.result, 1); @@ -177,7 +236,7 @@ pub async fn get_version(app: &App) -> Result { SELECT namespace, number FROM version WHERE valid_to IS NULL; " ) - .fetch_one(&app.database) + .fetch_one(pool) .await .context("Failed to fetch version number")?; @@ -185,7 +244,9 @@ pub async fn get_version(app: &App) -> Result { } pub async fn migrate_db(app: &App) -> Result<()> { - let current_version = get_version(app).await?; + let current_version = get_version(app) + .await + .context("Failed to determine initial version")?; if current_version == CURRENT_VERSION { return Ok(()); -- cgit 1.4.1