diff options
Diffstat (limited to 'yt/src/storage/migrate/mod.rs')
| -rw-r--r-- | yt/src/storage/migrate/mod.rs | 320 |
1 files changed, 0 insertions, 320 deletions
diff --git a/yt/src/storage/migrate/mod.rs b/yt/src/storage/migrate/mod.rs deleted file mode 100644 index badeb6f..0000000 --- a/yt/src/storage/migrate/mod.rs +++ /dev/null @@ -1,320 +0,0 @@ -// yt - A fully featured command line YouTube client -// -// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Yt. -// -// You should have received a copy of the License along with this program. -// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. - -use std::{ - fmt::Display, - future::Future, - time::{SystemTime, UNIX_EPOCH}, -}; - -use anyhow::{Context, Result, bail}; -use chrono::TimeDelta; -use log::{debug, info}; -use sqlx::{Sqlite, SqlitePool, Transaction, query}; - -use crate::app::App; - -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub enum DbVersion { - /// The database is not yet initialized. - Empty, - - /// The first database version. - /// Introduced: 2025-02-16. - Zero, - - /// Introduced: 2025-02-17. - One, - - /// Introduced: 2025-02-18. - Two, -} -const CURRENT_VERSION: DbVersion = DbVersion::Two; - -async fn add_error_context( - function: impl Future<Output = Result<()>>, - 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<DbVersion>, - new_version: DbVersion, -) -> Result<()> { - let valid_from = get_current_date(); - - if let Some(old_version) = old_version { - let valid_to = valid_from + 1; - let old_version = old_version.as_sql_integer(); - - query!( - "UPDATE version SET valid_to = ? WHERE namespace = 'yt' AND number = ?;", - valid_to, - old_version - ) - .execute(&mut *(*tx)) - .await?; - } - - let version = new_version.as_sql_integer(); - - query!( - "INSERT INTO version (namespace, number, valid_from, valid_to) VALUES ('yt', ?, ?, NULL);", - version, - valid_from - ) - .execute(&mut *(*tx)) - .await?; - - Ok(()) -} - -impl DbVersion { - fn as_sql_integer(self) -> i32 { - match self { - DbVersion::Empty => unreachable!("A empty version does not have an associated integer"), - DbVersion::Zero => 0, - DbVersion::One => 1, - DbVersion::Two => 2, - } - } - fn from_db(number: i64, namespace: &str) -> Result<Self> { - match (number, namespace) { - (0, "yt") => Ok(DbVersion::Zero), - (1, "yt") => Ok(DbVersion::One), - (2, "yt") => Ok(DbVersion::Two), - - (0, other) => bail!("Db version is Zero, but got unknown namespace: '{other}'"), - (1, other) => bail!("Db version is One, but got unknown namespace: '{other}'"), - (2, other) => bail!("Db version is Two, but got unknown namespace: '{other}'"), - - (other, "yt") => bail!("Got unkown version for 'yt' namespace: {other}"), - (num, nasp) => bail!("Got unkown version number ({num}) and namespace ('{nasp}')"), - } - } - - /// Try to update the database from version [`self`] to the [`CURRENT_VERSION`]. - /// - /// 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 => { - 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 => { - 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?; - - Box::pin(Self::One.update(app)).await - } - - DbVersion::One => { - add_error_context( - async { - let mut tx = app - .database - .begin() - .await - .context("Failed to start the update transaction")?; - debug!("Migrate: One -> Two"); - - sqlx::raw_sql(include_str!("./sql/02_one_to_two.sql")) - .execute(&mut *tx) - .await - .context("Failed to run the update sql script")?; - - set_db_version(&mut tx, Some(DbVersion::One), DbVersion::Two) - .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::One, - ) - .await?; - - Box::pin(Self::Two.update(app)) - .await - .context("Failed to update to version: Three") - } - - // This is the current_version - DbVersion::Two => { - assert_eq!(self, CURRENT_VERSION); - assert_eq!(self, get_version(app).await?); - Ok(()) - } - } - } -} -impl Display for DbVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // It is a unit only enum, thus we can simply use the Debug formatting - <Self as std::fmt::Debug>::fmt(self, f) - } -} - -/// Returns the current data as UNIX time stamp. -fn get_current_date() -> i64 { - let start = SystemTime::now(); - let seconds_since_epoch: TimeDelta = TimeDelta::from_std( - start - .duration_since(UNIX_EPOCH) - .expect("Time went backwards"), - ) - .expect("Time does not go backwards"); - - // All database dates should be after the UNIX_EPOCH (and thus positiv) - seconds_since_epoch.num_milliseconds() -} - -/// Return the current database version. -/// -/// # Panics -/// Only if internal assertions fail. -pub async fn get_version(app: &App) -> Result<DbVersion> { - 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<DbVersion> { - let version_table_exists = { - let query = query!( - "SELECT 1 as result FROM sqlite_master WHERE type = 'table' AND name = 'version'" - ) - .fetch_optional(pool) - .await?; - if let Some(output) = query { - assert_eq!(output.result, 1); - true - } else { - false - } - }; - if !version_table_exists { - return Ok(DbVersion::Empty); - } - - let current_version = query!( - " - SELECT namespace, number FROM version WHERE valid_to IS NULL; - " - ) - .fetch_one(pool) - .await - .context("Failed to fetch version number")?; - - DbVersion::from_db(current_version.number, current_version.namespace.as_str()) -} - -pub async fn migrate_db(app: &App) -> Result<()> { - let current_version = get_version(app) - .await - .context("Failed to determine initial version")?; - - if current_version == CURRENT_VERSION { - return Ok(()); - } - - info!("Migrate database from version '{current_version}' to version '{CURRENT_VERSION}'"); - - current_version.update(app).await?; - - Ok(()) -} |
