From b93d4f471ca147c3c236ff35202eed9475d53de2 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Sat, 14 Sep 2024 01:18:11 -0700 Subject: [PATCH] add 'db migrate' command option to specify target version --- src/cli.rs | 91 ++++++++++++++++++++++++++++++++++++++++++- src/cli/migrate_db.rs | 88 +++++++++++++++++++++++++++++++++-------- src/error.rs | 10 ++--- 3 files changed, 164 insertions(+), 25 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 5c0824d7..738fd61b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,7 @@ //! CLI argument structs are defined in this module. Execution logic for each //! command goes in a submodule. -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; use clap::{Parser, Subcommand}; @@ -78,6 +78,23 @@ pub(crate) struct MigrateDbArgs { #[clap(flatten)] config: ConfigArg, + /// Target server implementation to migrate database to. + /// + /// If migrating to the current version of grapevine, specify the version + /// as 'grapevine'. + /// + /// If migrating to a released version of conduit, specified the version + /// of conduit as `conduit-{version}` (example: `conduit-0.8.0`). If + /// migrating to an unreleased conduit build, instead specify the raw + /// database version as `conduit-db-{version}` (example: `conduit-db-13`). + /// The raw database version can be found by looking at the + /// `latest_database_version` variable in `src/database/mod.rs`. + /// + /// The server implementation used for the current database will be + /// detected automatically, and does not need to be specified. + #[clap(long)] + pub(crate) to: DbMigrationTarget, + /// Path to read database from. #[clap(long = "in", short)] pub(crate) in_path: PathBuf, @@ -87,6 +104,78 @@ pub(crate) struct MigrateDbArgs { pub(crate) out_path: PathBuf, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum DbMigrationTarget { + /// The latest grapevine db version + /// + /// Example: + /// + /// ``` + /// assert_eq!("grapevine".parse(), Ok(DbMigrationTarget::Grapevine)) + /// ``` + Grapevine, + /// A conduit-compatible db version. + /// + /// This may either be specified as a released version number or directly + /// as a database version. The raw database version must be used when + /// migrating to a conduit deployment built from an unreleased commit + /// on the `next` branch. + Conduit(ConduitDbVersion), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum ConduitDbVersion { + /// A conduit release version number + /// + /// Example: + /// + /// ``` + /// assert_eq!( + /// "conduit-0.8.0".parse(), + /// Ok(DbMigrationTarget::Conduit(ConduitDbVersion::Release("0.8.0"))) + /// ); + /// ``` + Release(String), + /// A raw database version + /// + /// This corresponds directly to a + /// [`crate::service::globals::DbVersion::Conduit`] version. + /// + /// Example: + /// + /// ``` + /// assert_eq!( + /// "conduit-db-13".parse(), + /// Ok(DbMigrationTarget::Conduit(ConduitDbVersion::Db(13))) + /// ); + /// ``` + Db(u64), +} + +#[derive(thiserror::Error, Debug)] +#[error("invalid db migration target version")] +pub(crate) struct DbMigrationTargetParseError; + +impl FromStr for DbMigrationTarget { + type Err = DbMigrationTargetParseError; + + fn from_str(s: &str) -> Result { + if s == "grapevine" { + Ok(DbMigrationTarget::Grapevine) + } else if let Some(version) = s.strip_prefix("conduit-db-") { + let version = + version.parse().map_err(|_| DbMigrationTargetParseError)?; + Ok(DbMigrationTarget::Conduit(ConduitDbVersion::Db(version))) + } else if let Some(version) = s.strip_prefix("conduit-") { + Ok(DbMigrationTarget::Conduit(ConduitDbVersion::Release( + version.to_owned(), + ))) + } else { + Err(DbMigrationTargetParseError) + } + } +} + impl Args { pub(crate) async fn run(self) -> Result<(), error::Main> { match self.command { diff --git a/src/cli/migrate_db.rs b/src/cli/migrate_db.rs index 8953ae45..2e012ce4 100644 --- a/src/cli/migrate_db.rs +++ b/src/cli/migrate_db.rs @@ -1,9 +1,34 @@ -use super::MigrateDbArgs; +use std::cmp::Ordering; + +use tracing::info; + +use super::{ConduitDbVersion, DbMigrationTarget, MigrateDbArgs}; use crate::{ config, database::KeyValueDatabase, error, observability, service::globals::DbVersion, services, utils::copy_dir, Services, }; +impl DbMigrationTarget { + fn to_db_version(&self) -> Result { + use error::MigrateDbCommand as Error; + + let latest_grapevine_version = DbVersion::Grapevine(0); + + match self { + DbMigrationTarget::Grapevine => Ok(latest_grapevine_version), + DbMigrationTarget::Conduit(ConduitDbVersion::Db(version)) => { + Ok(DbVersion::Conduit(*version)) + } + DbMigrationTarget::Conduit(ConduitDbVersion::Release(version)) => { + match &**version { + "0.8.0" => Ok(DbVersion::Conduit(13)), + _ => Err(Error::TargetVersionUnsupported), + } + } + } + } +} + pub(crate) async fn run( args: MigrateDbArgs, ) -> Result<(), error::MigrateDbCommand> { @@ -32,24 +57,53 @@ pub(crate) async fn run( services().globals.err_if_server_name_changed()?; - // Migrate from a grapevine-compatible db to a conduit-0.8.0-compatible db + let get_current = + || services().globals.database_version().map_err(Error::MigrateDb); + let current = get_current()?; + let target = args.to.to_db_version()?; + let latest = DbVersion::Grapevine(0); - let version = - services().globals.database_version().map_err(Error::MigrateDb)?; - if version < DbVersion::Grapevine(0) { - return Err(Error::DbVersionTooOld(version)); - } else if version != DbVersion::Grapevine(0) { - return Err(Error::DbVersionUnsupported(version)); - }; + info!("Migrating from {current:?} to {target:?}"); - // Undo Conduit(13) -> Grapevine(0) - // - // This is a no-op that only changes the db version namespace. Setting the - // version to Conduit(_) will restore the original state. - services() - .globals - .bump_database_version(DbVersion::Conduit(13)) - .map_err(Error::MigrateDb)?; + if target == current { + // No-op + } else if target == latest { + // Migrate to latest grapevine + if !current.partial_cmp(&latest).is_some_and(Ordering::is_le) { + return Err(Error::DbVersionUnsupported(current)); + } + db.apply_migrations().await.map_err(Error::MigrateDb)?; + } else if target == DbVersion::Conduit(13) { + // Migrate to latest grapevine so we have a consistent starting point + if !current.partial_cmp(&latest).is_some_and(Ordering::is_le) { + return Err(Error::DbVersionUnsupported(current)); + } + db.apply_migrations().await.map_err(Error::MigrateDb)?; + assert_eq!( + get_current()?, + latest, + "should have migrated to latest version" + ); + + // Undo Conduit(13) -> Grapevine(0) + // + // This is a no-op that only changes the db version namespace. Setting + // the version to Conduit(_) will restore the original state. + services() + .globals + .bump_database_version(DbVersion::Conduit(13)) + .map_err(Error::MigrateDb)?; + } else { + return Err(Error::TargetVersionUnsupported); + } + + assert_eq!( + get_current()?, + target, + "should have migrated to target version" + ); + + info!("Migration successful"); Ok(()) } diff --git a/src/error.rs b/src/error.rs index a07671ed..9d1b1df5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -118,15 +118,11 @@ pub(crate) enum MigrateDbCommand { #[error("failed to migrate database")] MigrateDb(#[source] crate::utils::error::Error), - #[error( - "initial database version is too old for migration: {_0:?}. Try \ - loading the database with the latest conduit release and then \ - attempting migration again." - )] - DbVersionTooOld(DbVersion), - #[error("initial database version is not supported for migration: {_0:?}")] DbVersionUnsupported(DbVersion), + + #[error("target database version is not supported for migration")] + TargetVersionUnsupported, } /// Errors copying a directory recursively.