Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detach latest migration #100

Merged
merged 8 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions butane_cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,47 @@ pub fn make_migration(base_dir: &PathBuf, name: Option<&String>) -> Result<()> {
Ok(())
}

/// Detach the latest migration from the list of migrations,
/// leaving the migration on the filesystem.
pub fn detach_latest_migration(base_dir: &PathBuf) -> Result<()> {
let mut ms = get_migrations(base_dir)?;
let all_migrations = ms.all_migrations().unwrap_or_else(|e| {
eprintln!("Error: {e}");
std::process::exit(1);
});
let initial_migration = all_migrations.first().unwrap_or_else(|| {
eprintln!("There are no migrations");
std::process::exit(1);
});
let top_migration = ms.latest().expect("Latest should exist");
if initial_migration == &top_migration {
eprintln!("Can not detach initial migration");
std::process::exit(1);
}
if let Ok(spec) = db::ConnectionSpec::load(base_dir) {
let conn = db::connect(&spec)?;
if let Some(top_applied_migration) = ms.last_applied_migration(&conn)? {
if top_applied_migration == top_migration {
eprintln!("Can not detach an applied migration");
std::process::exit(1);
}
}
}
let previous_migration = &all_migrations[all_migrations.len() - 2];
println!(
"Detaching {} from {}",
top_migration.name(),
previous_migration.name()
);
ms.detach_latest_migration()?;
let cli_state = CliState::load(base_dir)?;
if cli_state.embedded {
// The latest migration needs to be removed from the embedding
embed(base_dir)?;
}
Ok(())
}

pub fn migrate(base_dir: &PathBuf) -> Result<()> {
let spec = load_connspec(base_dir)?;
let mut conn = db::connect(&spec)?;
Expand Down
36 changes: 34 additions & 2 deletions butane_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::path::PathBuf;
use clap::{value_parser, Arg, ArgMatches};

use butane_cli::{
base_dir, clean, clear_data, collapse_migrations, delete_table, embed, handle_error,
list_migrations, migrate, Result,
base_dir, clean, clear_data, collapse_migrations, delete_table, detach_latest_migration, embed,
get_migrations, handle_error, list_migrations, migrate, Result,
};

fn main() {
Expand Down Expand Up @@ -49,6 +49,22 @@ fn main() {
.help("Name to use for the migration"),
),
)
.subcommand(
clap::Command::new("detach-migration")
.about("Detach the latest migration")
Electron100 marked this conversation as resolved.
Show resolved Hide resolved
.alias("detachmigration")
.after_help(r#"This command removes the latest migration from the list of migrations and sets butane state to before the latest migration was created.

The removed migration is not deleted from file system.

This operation is the first step of the process of rebasing a migration onto other migrations that have the same original migration.

If the migration has not been manually edited, it can be automatically regenerated after being rebased. In this case, deleting the detached migration is often the best approach.

However if the migration has been manually edited, it will need to be manually re-attached to the target migration series after the rebase has been completed.
"#
)
)
.subcommand(clap::Command::new("migrate").about("Apply migrations"))
.subcommand(clap::Command::new("list").about("List migrations"))
.subcommand(clap::Command::new("collapse").about("Replace all migrations with a single migration representing the current model state.").arg(
Expand Down Expand Up @@ -98,11 +114,27 @@ fn main() {
let args = app.get_matches();
let mut base_dir = args.get_one::<PathBuf>("path").unwrap().clone();
base_dir.push(".butane");

// List any detached migrations.
if let Ok(ms) = get_migrations(&base_dir) {
if let Ok(detached_migrations) = ms.detached_migration_paths() {
if !detached_migrations.is_empty() {
eprintln!(
"Ignoring detached migrations. Please delete or manually re-attach these:"
);
for migration in detached_migrations {
eprintln!("- {migration}");
}
}
};
};

match args.subcommand() {
Some(("init", sub_args)) => handle_error(init(&base_dir, Some(sub_args))),
Some(("makemigration", sub_args)) => {
handle_error(make_migration(&base_dir, Some(sub_args)))
}
Some(("detach-migration", _)) => handle_error(detach_latest_migration(&base_dir)),
Some(("migrate", _)) => handle_error(migrate(&base_dir)),
Some(("rollback", sub_args)) => handle_error(rollback(&base_dir, Some(sub_args))),
Some(("embed", _)) => handle_error(embed(&base_dir)),
Expand Down
36 changes: 35 additions & 1 deletion butane_core/src/migrations/fsmigrations.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::adb::{ATable, DeferredSqlType, TypeKey, ADB};
use super::fs::{Filesystem, OsFilesystem};
use super::{Migration, MigrationMut, Migrations, MigrationsMut};
use crate::{ConnectionMethods, DataObject, Result};
use crate::{ConnectionMethods, DataObject, Error, Result};
use fs2::FileExt;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
Expand Down Expand Up @@ -271,6 +271,40 @@ impl FsMigrations {
f.write_all(serde_json::to_string(state)?.as_bytes())
.map_err(|e| e.into())
}
/// Detach the latest migration from the list of migrations,
/// leaving the migration on the filesystem.
pub fn detach_latest_migration(&mut self) -> Result<()> {
Electron100 marked this conversation as resolved.
Show resolved Hide resolved
let latest = self
.latest()
.ok_or(Error::MigrationError("There are no migrations".to_string()))?;
let from_name =
latest
.migration_from()?
.map(|s| s.to_string())
.ok_or(Error::MigrationError(
"There is no previous migration".to_string(),
))?;
let mut state = self.get_state()?;
state.latest = Some(from_name);
self.save_state(&state)?;
Ok(())
}
/// Provides a Vec of migration directories that have been detached.
pub fn detached_migration_paths(&self) -> Result<Vec<String>> {
let migration_series = self.all_migrations()?;
let mut detached_directory_names: Vec<String> = vec![];
for entry in std::fs::read_dir(self.root.clone())? {
let path = entry?.path();
let name = path.file_name().unwrap();
if !path.is_dir() || name == "current" {
continue;
}
if !migration_series.iter().any(|item| item.root == path) {
detached_directory_names.push(path.display().to_string());
};
}
Ok(detached_directory_names)
}
}

impl Migrations for FsMigrations {
Expand Down