add 'db migrate' command option to specify target version

This commit is contained in:
Benjamin Lee 2024-09-14 01:18:11 -07:00
parent 6446822bf2
commit b93d4f471c
No known key found for this signature in database
GPG key ID: FB9624E2885D55A4
3 changed files with 164 additions and 25 deletions

View file

@ -3,7 +3,7 @@
//! CLI argument structs are defined in this module. Execution logic for each //! CLI argument structs are defined in this module. Execution logic for each
//! command goes in a submodule. //! command goes in a submodule.
use std::path::PathBuf; use std::{path::PathBuf, str::FromStr};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
@ -78,6 +78,23 @@ pub(crate) struct MigrateDbArgs {
#[clap(flatten)] #[clap(flatten)]
config: ConfigArg, 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. /// Path to read database from.
#[clap(long = "in", short)] #[clap(long = "in", short)]
pub(crate) in_path: PathBuf, pub(crate) in_path: PathBuf,
@ -87,6 +104,78 @@ pub(crate) struct MigrateDbArgs {
pub(crate) out_path: PathBuf, 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<Self, Self::Err> {
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 { impl Args {
pub(crate) async fn run(self) -> Result<(), error::Main> { pub(crate) async fn run(self) -> Result<(), error::Main> {
match self.command { match self.command {

View file

@ -1,9 +1,34 @@
use super::MigrateDbArgs; use std::cmp::Ordering;
use tracing::info;
use super::{ConduitDbVersion, DbMigrationTarget, MigrateDbArgs};
use crate::{ use crate::{
config, database::KeyValueDatabase, error, observability, config, database::KeyValueDatabase, error, observability,
service::globals::DbVersion, services, utils::copy_dir, Services, service::globals::DbVersion, services, utils::copy_dir, Services,
}; };
impl DbMigrationTarget {
fn to_db_version(&self) -> Result<DbVersion, error::MigrateDbCommand> {
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( pub(crate) async fn run(
args: MigrateDbArgs, args: MigrateDbArgs,
) -> Result<(), error::MigrateDbCommand> { ) -> Result<(), error::MigrateDbCommand> {
@ -32,24 +57,53 @@ pub(crate) async fn run(
services().globals.err_if_server_name_changed()?; 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 = info!("Migrating from {current:?} to {target:?}");
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));
};
// Undo Conduit(13) -> Grapevine(0) if target == current {
// // No-op
// This is a no-op that only changes the db version namespace. Setting the } else if target == latest {
// version to Conduit(_) will restore the original state. // Migrate to latest grapevine
services() if !current.partial_cmp(&latest).is_some_and(Ordering::is_le) {
.globals return Err(Error::DbVersionUnsupported(current));
.bump_database_version(DbVersion::Conduit(13)) }
.map_err(Error::MigrateDb)?; 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(()) Ok(())
} }

View file

@ -118,15 +118,11 @@ pub(crate) enum MigrateDbCommand {
#[error("failed to migrate database")] #[error("failed to migrate database")]
MigrateDb(#[source] crate::utils::error::Error), 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:?}")] #[error("initial database version is not supported for migration: {_0:?}")]
DbVersionUnsupported(DbVersion), DbVersionUnsupported(DbVersion),
#[error("target database version is not supported for migration")]
TargetVersionUnsupported,
} }
/// Errors copying a directory recursively. /// Errors copying a directory recursively.