Skip to content

feat!: Add support for rolling back migrations#583

Open
ElijahAhianyo wants to merge 14 commits into
masterfrom
elijah/migrations-rollback
Open

feat!: Add support for rolling back migrations#583
ElijahAhianyo wants to merge 14 commits into
masterfrom
elijah/migrations-rollback

Conversation

@ElijahAhianyo

@ElijahAhianyo ElijahAhianyo commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Related issue or discussion

fixes #547

Description

This allows rolling back migrations to the specified migration file.

Assuming the project has the following migration files in its migration dir:

├── migrations
 │   └── m_0001_initial.rs
 |   └── m_0002_second.rs

./target/debug/foo migration rollback m_0001_initial

The above command will roll back(unapply) the m_0002_second file, leaving m_0001_initial applied.
Alternatively, the migration file number can be used for convenience:

./target/debug/foo migration rollback 0001

Migrations are currently tied to a crate(the crate name is used), which means rolling back a migration in a project that imports apps from external crates will implicitly (and only) rollback migrations in the current project/crate. This might change when support for workspaces is added

As a bonus, this PR also adds CLI Group tasks as a means to group nested related subcommands under a parent subcommand:

Eg.

     use async_trait::async_trait;
     use clap::{ArgMatches, Command};
     use cot::cli::{Cli, CliTask, CliTaskGroup};
     use cot::project::WithConfig;
     use cot::{Bootstrapper, Project};
    
     struct Frobnicate;
    
     #[async_trait(?Send)]
     impl CliTask for Frobnicate {
         fn subcommand(&self) -> Command {
             Command::new("frobnicate")
                  .about("Test frobnicate command")

         }
    
         async fn execute(
             &mut self,
             _matches: &ArgMatches,
             _bootstrapper: Bootstrapper<WithConfig>,
         ) -> cot::Result<()> {
             println!("Frobnicating...");
    
             Ok(())
         }
     }
    
     struct MyProject;
     impl Project for MyProject {
         fn register_tasks(&self, cli: &mut Cli) {
             let mut group_command = CliTaskGroup::new("foo").about("Foo related commands");
             group_command.add_task(Frobnicate);
             cli.add_task(group_command);
        }
   }

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor / cleanup
  • Performance improvement
  • Other (describe above)

@github-actions github-actions Bot added A-deps Area: Dependencies C-lib Crate: cot (main library crate) labels Jun 2, 2026
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchelijah/migrations-rollback
Testbedgithub-ubuntu-latest
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
empty_router/empty_router📈 view plot
🚷 view threshold
5,918.90 µs
(-4.04%)Baseline: 6,168.10 µs
7,754.14 µs
(76.33%)
json_api/json_api📈 view plot
🚷 view threshold
996.84 µs
(-7.77%)Baseline: 1,080.84 µs
1,322.26 µs
(75.39%)
nested_routers/nested_routers📈 view plot
🚷 view threshold
967.47 µs
(-3.23%)Baseline: 999.76 µs
1,202.64 µs
(80.45%)
single_root_route/single_root_route📈 view plot
🚷 view threshold
931.94 µs
(-3.16%)Baseline: 962.36 µs
1,165.95 µs
(79.93%)
single_root_route_burst/single_root_route_burst📈 view plot
🚷 view threshold
16,320.00 µs
(-7.66%)Baseline: 17,674.11 µs
21,077.68 µs
(77.43%)
🐰 View full continuous benchmarking report in Bencher

@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.51969% with 19 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cot/src/db/migrations.rs 90.00% 9 Missing and 8 partials ⚠️
cot/src/cli.rs 97.26% 2 Missing ⚠️
Flag Coverage Δ
rust 90.19% <92.51%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cot/src/db/migrations/sorter.rs 93.59% <100.00%> (+0.12%) ⬆️
cot/src/utils/graph.rs 98.76% <100.00%> (+0.04%) ⬆️
cot/src/cli.rs 86.73% <97.26%> (+3.47%) ⬆️
cot/src/db/migrations.rs 84.97% <90.00%> (+1.52%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ElijahAhianyo ElijahAhianyo marked this pull request as ready for review June 9, 2026 20:46
@ElijahAhianyo ElijahAhianyo requested review from m4tx and seqre June 9, 2026 20:58

@m4tx m4tx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a solid start, but we need more work on this before it can be merged.

Comment thread cot/src/cli.rs
// name.
// TODO: cli command should take an explicit crate name as arg when workspaces
// are supported.
let crate_name = bootstrapper.project().cli_metadata().name;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can still potentially want to rollback other libraries' migrations, so I guess we should still support other crates here. For instance, in the admin example we have on the repo, we should still support rolling back cot's m_0001_initial that creates cot__database_user. This is perhaps not terribly useful on its own, but if someone uses libraries that define some more complex migrations, that's more useful. And I believe should be fairly simple to implement, too.

Also, another problem with this in the current form is that we can have multiple migrations called m_0001_initial and it's inclear which one will be chosen.

Comment thread cot/src/cli.rs
.with_apps()
.with_database()
.await?
.with_cache()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to skip .with_cache() in the chain here and call boot() directly.

Comment thread cot/src/cli.rs
}
}

struct MigrationRollback;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe a few features would make it much more useful:

  1. A subcommand that would list all available migrations
  2. A dry run mode to see which migrations would actually be rolled back
  3. Actual log lines being displayed. cot-cli just eprintlns the info when executing migration make, which I think we could replicate here.

Comment thread cot/src/cli.rs
bootstrapper: Bootstrapper<WithConfig>,
) -> Result<()> {
let file = matches
.get_one::<String>("file")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be called "migration name"? I know this is usually the same as file name, but doesn't necessarily have to be.

Comment thread cot/src/db/migrations.rs
/// database or if there is an error while generating the migration
/// graph or if there is an error while unapplying a migration.
pub async fn rollback(&self, database: &Database, file: &str, app_name: &str) -> Result<()> {
info!("Rolling back migrations");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a TODO to add transactions here. And I should finally finish the work on #468...

Sadly, the lack of transactions means that we can potentially leave the database in an invalid state if one of the rollback fails. I'm wondering if the transactions would fix this issue, or are there any database engines that we support that don't support transactions on schema-changing operations? (SQLite could be one, but I'm not sure about the status in 2026)

Comment thread cot/src/cli.rs

let mut migration_group =
CliTaskGroup::new(MIGRATION_GROUP_SUBCOMMAND).about("Database migration commands");
migration_group.add_task(MigrationRollback);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The struct and its registration should be gated behind #[cfg(feature = "db)].

Comment thread cot/src/db/migrations.rs
operation.backwards(database).await?;
}

Self::mark_migration_unapplied(database, migration).await?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we can rollback already rolled back migrations - this shouldn't be possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-deps Area: Dependencies C-lib Crate: cot (main library crate)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add command to rollback migration

2 participants