Skip to content

Commit 9f2d073

Browse files
authored
Add rake task to delete broken migrations (#141)
1 parent 77fbb92 commit 9f2d073

File tree

4 files changed

+200
-1
lines changed

4 files changed

+200
-1
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,49 @@ remove_index :users, :email
247247
rename_column :users, :username, :handle
248248
```
249249

250+
## Delete Broken Migrations
251+
252+
A migration is considered broken if it has been migrated in the database but the corresponding migration file is missing. This functionality allows you to safely delete these broken versions from the database to keep it clean.
253+
254+
You can delete broken migrations using either of the following methods:
255+
256+
### 1. Using the UI
257+
258+
Navigate to the following URL in your web browser:
259+
```
260+
http://localhost:3000/rails/broken_versions
261+
```
262+
263+
This page lists all broken versions and provides an option to delete them.
264+
265+
### 2. Using a Rake Task
266+
267+
To delete all broken migrations, run:
268+
```sh
269+
rake actual_db_schema:delete_broken_versions
270+
```
271+
272+
To delete specific migrations, pass the migration version(s) and optionally a database:
273+
```sh
274+
rake actual_db_schema:delete_broken_versions[<version>, <version>]
275+
```
276+
277+
- `<version>` – The migration version(s) to delete (space-separated if multiple).
278+
- `<database>` (optional) – Specify a database if using multiple databases.
279+
280+
#### Examples:
281+
282+
```sh
283+
# Delete all broken migrations
284+
rake actual_db_schema:delete_broken_versions
285+
286+
# Delete specific migrations
287+
rake actual_db_schema:delete_broken_versions["20250224103352 20250224103358"]
288+
289+
# Delete specific migrations from a specific database
290+
rake actual_db_schema:delete_broken_versions["20250224103352 20250224103358", "primary"]
291+
```
292+
250293
## Development
251294

252295
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

lib/actual_db_schema/migration.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,11 @@ def broken_versions
9393
end
9494

9595
def delete(version, database)
96+
validate_broken_migration(version, database)
97+
9698
MigrationContext.instance.each do
97-
next unless ActualDbSchema.db_config[:database] == database
99+
next if database && ActualDbSchema.db_config[:database] != database
100+
next if ActiveRecord::Base.connection.select_values("SELECT version FROM schema_migrations").exclude?(version)
98101

99102
ActiveRecord::Base.connection.execute("DELETE FROM schema_migrations WHERE version = '#{version}'")
100103
break
@@ -150,5 +153,15 @@ def metadata
150153
@metadata ||= {}
151154
@metadata[ActualDbSchema.db_config[:database]] ||= ActualDbSchema::Store.instance.read
152155
end
156+
157+
def validate_broken_migration(version, database)
158+
if database
159+
unless broken_versions.any? { |v| v.version == version && v.database == database }
160+
raise StandardError, "Migration is not broken for database #{database}."
161+
end
162+
else
163+
raise StandardError, "Migration is not broken." unless broken_versions.any? { |v| v.version == version }
164+
end
165+
end
153166
end
154167
end

lib/tasks/actual_db_schema.rake

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,28 @@ namespace :actual_db_schema do # rubocop:disable Metrics/BlockLength
6262
schema_diff = ActualDbSchema::SchemaDiff.new(schema_path, migrations_path)
6363
puts schema_diff.render
6464
end
65+
66+
desc "Delete broken migration versions from the database"
67+
task :delete_broken_versions, %i[versions database] => :environment do |_, args|
68+
extend ActualDbSchema::OutputFormatter
69+
70+
if args[:versions]
71+
versions = args[:versions].split(" ").map(&:strip)
72+
versions.each do |version|
73+
ActualDbSchema::Migration.instance.delete(version, args[:database])
74+
puts colorize("[ActualDbSchema] Migration #{version} was successfully deleted.", :green)
75+
rescue StandardError => e
76+
puts colorize("[ActualDbSchema] Error deleting version #{version}: #{e.message}", :red)
77+
end
78+
elsif ActualDbSchema::Migration.instance.broken_versions.empty?
79+
puts colorize("[ActualDbSchema] No broken versions found.", :gray)
80+
else
81+
begin
82+
ActualDbSchema::Migration.instance.delete_all
83+
puts colorize("[ActualDbSchema] All broken versions were successfully deleted.", :green)
84+
rescue StandardError => e
85+
puts colorize("[ActualDbSchema] Error deleting all broken versions: #{e.message}", :red)
86+
end
87+
end
88+
end
6589
end
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
describe "actual_db_schema:delete_broken_versions" do
6+
let(:utils) do
7+
TestUtils.new(
8+
migrations_path: ["db/migrate", "db/migrate_secondary"],
9+
migrated_path: ["tmp/migrated", "tmp/migrated_migrate_secondary"]
10+
)
11+
end
12+
13+
before do
14+
utils.reset_database_yml(TestingState.db_config)
15+
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
16+
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
17+
utils.cleanup(TestingState.db_config)
18+
utils.run_migrations
19+
end
20+
21+
def delete_migration_files
22+
utils.remove_app_dir(Rails.root.join("db", "migrate", "20130906111511_first_primary.rb"))
23+
utils.remove_app_dir(Rails.root.join("db", "migrate_secondary", "20130906111514_first_secondary.rb"))
24+
utils.remove_app_dir(Rails.root.join("tmp", "migrated", "20130906111511_first_primary.rb"))
25+
utils.remove_app_dir(Rails.root.join("tmp", "migrated_migrate_secondary", "20130906111514_first_secondary.rb"))
26+
end
27+
28+
describe "when versions are provided" do
29+
before { delete_migration_files }
30+
31+
it "deletes the specified broken migrations" do
32+
sql = "SELECT COUNT(*) FROM schema_migrations"
33+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
34+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
35+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
36+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
37+
Rake::Task["actual_db_schema:delete_broken_versions"].invoke("20130906111511 20130906111514")
38+
Rake::Task["actual_db_schema:delete_broken_versions"].reenable
39+
assert_match(/\[ActualDbSchema\] Migration 20130906111511 was successfully deleted./, TestingState.output)
40+
assert_match(/\[ActualDbSchema\] Migration 20130906111514 was successfully deleted./, TestingState.output)
41+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
42+
assert_equal 1, ActiveRecord::Base.connection.select_value(sql)
43+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
44+
assert_equal 1, ActiveRecord::Base.connection.select_value(sql)
45+
end
46+
47+
it "deletes broken migrations only from the given database when specified" do
48+
sql = "SELECT COUNT(*) FROM schema_migrations"
49+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
50+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
51+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
52+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
53+
Rake::Task["actual_db_schema:delete_broken_versions"]
54+
.invoke("20130906111511 20130906111514", TestingState.db_config["primary"]["database"])
55+
Rake::Task["actual_db_schema:delete_broken_versions"].reenable
56+
assert_match(/\[ActualDbSchema\] Migration 20130906111511 was successfully deleted./, TestingState.output)
57+
assert_match(
58+
/\[ActualDbSchema\] Error deleting version 20130906111514: Migration is not broken for database #{TestingState.db_config["primary"]["database"]}./, # rubocop:disable Layout/LineLength
59+
TestingState.output
60+
)
61+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
62+
assert_equal 1, ActiveRecord::Base.connection.select_value(sql)
63+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
64+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
65+
end
66+
67+
it "prints an error message when the passed version is not broken" do
68+
Rake::Task["actual_db_schema:delete_broken_versions"].invoke("20130906111512")
69+
Rake::Task["actual_db_schema:delete_broken_versions"].reenable
70+
assert_match(
71+
/\[ActualDbSchema\] Error deleting version 20130906111512: Migration is not broken./, TestingState.output
72+
)
73+
end
74+
end
75+
76+
describe "when no versions are provided" do
77+
before { delete_migration_files }
78+
79+
it "deletes all broken migrations" do
80+
delete_migration_files
81+
sql = "SELECT COUNT(*) FROM schema_migrations"
82+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
83+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
84+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
85+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
86+
Rake::Task["actual_db_schema:delete_broken_versions"].invoke
87+
Rake::Task["actual_db_schema:delete_broken_versions"].reenable
88+
assert_match(/\[ActualDbSchema\] All broken versions were successfully deleted./, TestingState.output)
89+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
90+
assert_equal 1, ActiveRecord::Base.connection.select_value(sql)
91+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
92+
assert_equal 1, ActiveRecord::Base.connection.select_value(sql)
93+
end
94+
95+
it "prints an error message if there is an error during deletion" do
96+
original_delete_all = ActualDbSchema::Migration.instance_method(:delete_all)
97+
ActualDbSchema::Migration.define_method(:delete_all) do
98+
raise StandardError, "Deletion error"
99+
end
100+
Rake::Task["actual_db_schema:delete_broken_versions"].invoke
101+
Rake::Task["actual_db_schema:delete_broken_versions"].reenable
102+
assert_match(/\[ActualDbSchema\] Error deleting all broken versions: Deletion error/, TestingState.output)
103+
sql = "SELECT COUNT(*) FROM schema_migrations"
104+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
105+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
106+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
107+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
108+
ActualDbSchema::Migration.define_method(:delete_all, original_delete_all)
109+
end
110+
end
111+
112+
describe "when there are no broken versions" do
113+
it "prints a message indicating no broken versions found" do
114+
Rake::Task["actual_db_schema:delete_broken_versions"].invoke
115+
Rake::Task["actual_db_schema:delete_broken_versions"].reenable
116+
assert_match(/No broken versions found/, TestingState.output)
117+
end
118+
end
119+
end

0 commit comments

Comments
 (0)