Skip to content

Commit 77fbb92

Browse files
authored
Delete broken migrations (#140)
1 parent 6416ca0 commit 77fbb92

File tree

10 files changed

+283
-6
lines changed

10 files changed

+283
-6
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
module ActualDbSchema
4+
# Controller for managing broken migration versions.
5+
class BrokenVersionsController < ActionController::Base
6+
protect_from_forgery with: :exception
7+
skip_before_action :verify_authenticity_token
8+
9+
def index; end
10+
11+
def delete
12+
handle_delete(params[:id], params[:database])
13+
redirect_to broken_versions_path
14+
end
15+
16+
def delete_all
17+
handle_delete_all
18+
redirect_to broken_versions_path
19+
end
20+
21+
private
22+
23+
def handle_delete(id, database)
24+
ActualDbSchema::Migration.instance.delete(id, database)
25+
flash[:notice] = "Migration #{id} was successfully deleted."
26+
rescue StandardError => e
27+
flash[:alert] = e.message
28+
end
29+
30+
def handle_delete_all
31+
ActualDbSchema::Migration.instance.delete_all
32+
flash[:notice] = "All broken versions were successfully deleted."
33+
rescue StandardError => e
34+
flash[:alert] = e.message
35+
end
36+
37+
helper_method def broken_versions
38+
@broken_versions ||= ActualDbSchema::Migration.instance.broken_versions
39+
end
40+
end
41+
end
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Broken Versions</title>
5+
<%= render partial: 'actual_db_schema/shared/js' %>
6+
<%= render partial: 'actual_db_schema/shared/style' %>
7+
</head>
8+
<body>
9+
<div>
10+
<% flash.each do |key, message| %>
11+
<div class="flash <%= key %>"><%= message %></div>
12+
<% end %>
13+
<h2>Broken Versions</h2>
14+
<p>
15+
These are versions that were migrated in the database, but the corresponding migration file is missing.
16+
You can safely delete them from the database to clean up.
17+
</p>
18+
<div class="top-buttons">
19+
<%= link_to 'All Migrations', migrations_path, class: "top-button" %>
20+
<% if broken_versions.present? %>
21+
<%= button_to '✖ Delete all',
22+
delete_all_broken_versions_path,
23+
method: :post,
24+
data: { confirm: 'These migrations do not have corresponding migration files. Proceeding will remove these entries from the `schema_migrations` table. Are you sure you want to continue?' },
25+
class: 'button migration-action' %>
26+
<% end %>
27+
</div>
28+
<% if broken_versions.present? %>
29+
<table>
30+
<thead>
31+
<tr>
32+
<th>Status</th>
33+
<th>Migration ID</th>
34+
<th>Branch</th>
35+
<th>Database</th>
36+
<th>Actions</th>
37+
</tr>
38+
</thead>
39+
<tbody>
40+
<% broken_versions.each do |version| %>
41+
<tr class="migration-row phantom">
42+
<td><%= version[:status] %></td>
43+
<td><%= version[:version] %></td>
44+
<td><%= version[:branch] %></td>
45+
<td><%= version[:database] %></td>
46+
<td>
47+
<div class='button-container'>
48+
<%= button_to '✖ Delete',
49+
delete_broken_version_path(id: version[:version], database: version[:database]),
50+
method: :post,
51+
data: { confirm: 'This migration does not have a corresponding migration file. Proceeding will remove its entry from the `schema_migrations` table. Are you sure you want to continue?' },
52+
class: 'button migration-action' %>
53+
</div>
54+
</td>
55+
</tr>
56+
<% end %>
57+
</tbody>
58+
</table>
59+
<% else %>
60+
<p>No broken versions found.</p>
61+
<% end %>
62+
</div>
63+
</body>
64+
</html>

app/views/actual_db_schema/migrations/index.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<div class="top-controls">
1919
<div class="top-buttons">
2020
<%= link_to 'Phantom Migrations', phantom_migrations_path, class: "top-button" %>
21+
<%= link_to 'Broken Versions', broken_versions_path, class: "top-button" %>
2122
<%= link_to 'View Schema', schema_path, class: "top-button" %>
2223
</div>
2324
<div class="top-search">

app/views/actual_db_schema/shared/_js.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
migrationActions.forEach(button => {
66
button.addEventListener('click', function(event) {
7+
const confirmMessage = button.dataset.confirm;
8+
if (confirmMessage && !confirm(confirmMessage)) {
9+
event.preventDefault();
10+
return;
11+
}
12+
713
const originalText = button.value;
814
button.value = 'Loading...';
915
disableButtons();

app/views/actual_db_schema/shared/_style.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
padding-left: 10px;
1616
}
1717

18+
p {
19+
padding-left: 10px;
20+
}
21+
1822
table {
1923
margin: 0;
2024
border-collapse: collapse;

config/routes.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
post :rollback_all
1616
end
1717
end
18+
resources :broken_versions, only: %i[index] do
19+
member do
20+
post :delete
21+
end
22+
collection do
23+
post :delete_all
24+
end
25+
end
1826

1927
get "schema", to: "schema#index", as: :schema
2028
end

lib/actual_db_schema/migration.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,40 @@ def migrate(version, database)
7373
end
7474
end
7575

76+
def broken_versions
77+
broken = []
78+
MigrationContext.instance.each do |context|
79+
context.migrations_status.each do |status, version, name|
80+
next unless name == "********** NO FILE **********"
81+
82+
broken << Migration.new(
83+
status: status,
84+
version: version.to_s,
85+
name: name,
86+
branch: branch_for(version),
87+
database: ActualDbSchema.db_config[:database]
88+
)
89+
end
90+
end
91+
92+
broken
93+
end
94+
95+
def delete(version, database)
96+
MigrationContext.instance.each do
97+
next unless ActualDbSchema.db_config[:database] == database
98+
99+
ActiveRecord::Base.connection.execute("DELETE FROM schema_migrations WHERE version = '#{version}'")
100+
break
101+
end
102+
end
103+
104+
def delete_all
105+
broken_versions.each do |version|
106+
delete(version.version, version.database)
107+
end
108+
end
109+
76110
private
77111

78112
def build_migration_struct(status, migration)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../test_helper"
4+
require_relative "../../../app/controllers/actual_db_schema/broken_versions_controller"
5+
6+
module ActualDbSchema
7+
class BrokenVersionsControllerTest < ActionController::TestCase
8+
def setup
9+
@utils = TestUtils.new
10+
@app = Rails.application
11+
routes_setup
12+
Rails.logger = Logger.new($stdout)
13+
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
14+
active_record_setup
15+
@utils.reset_database_yml(TestingState.db_config)
16+
@utils.cleanup(TestingState.db_config)
17+
@utils.prepare_phantom_migrations(TestingState.db_config)
18+
end
19+
20+
def routes_setup
21+
@routes = @app.routes
22+
Rails.application.routes.draw do
23+
get "/rails/broken_versions" => "actual_db_schema/broken_versions#index", as: "broken_versions"
24+
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
25+
post "/rails/broken_version/:id/delete" => "actual_db_schema/broken_versions#delete",
26+
as: "delete_broken_version"
27+
post "/rails/broken_versions/delete_all" => "actual_db_schema/broken_versions#delete_all",
28+
as: "delete_all_broken_versions"
29+
end
30+
ActualDbSchema::BrokenVersionsController.include(@routes.url_helpers)
31+
end
32+
33+
def active_record_setup
34+
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
35+
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
36+
end
37+
38+
def delete_migrations_files
39+
@utils.delete_migrations_files_for("tmp/migrated")
40+
@utils.delete_migrations_files_for("tmp/migrated_migrate_secondary")
41+
end
42+
43+
test "GET #index returns a successful response" do
44+
delete_migrations_files
45+
get :index
46+
assert_response :success
47+
assert_select "table" do
48+
assert_select "tbody" do
49+
assert_select "tr" do
50+
assert_select "td", text: "up"
51+
assert_select "td", text: "20130906111511"
52+
assert_select "td", text: @utils.branch_for("20130906111511")
53+
assert_select "td", text: @utils.primary_database
54+
end
55+
assert_select "tr" do
56+
assert_select "td", text: "up"
57+
assert_select "td", text: "20130906111512"
58+
assert_select "td", text: @utils.branch_for("20130906111512")
59+
assert_select "td", text: @utils.primary_database
60+
end
61+
assert_select "tr" do
62+
assert_select "td", text: "up"
63+
assert_select "td", text: "20130906111514"
64+
assert_select "td", text: @utils.branch_for("20130906111514")
65+
assert_select "td", text: @utils.secondary_database
66+
end
67+
assert_select "tr" do
68+
assert_select "td", text: "up"
69+
assert_select "td", text: "20130906111515"
70+
assert_select "td", text: @utils.branch_for("20130906111515")
71+
assert_select "td", text: @utils.secondary_database
72+
end
73+
end
74+
end
75+
end
76+
77+
test "GET #index when there are no broken versions returns a not found text" do
78+
get :index
79+
assert_response :success
80+
assert_select "p", text: "No broken versions found."
81+
end
82+
83+
test "POST #delete removes migration entry from the schema_migrations table" do
84+
delete_migrations_files
85+
version = "20130906111511"
86+
sql = "SELECT version FROM schema_migrations WHERE version = '#{version}'"
87+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
88+
assert_not_nil ActiveRecord::Base.connection.select_value(sql)
89+
90+
post :delete, params: { id: "20130906111511", database: @utils.primary_database }
91+
assert_response :redirect
92+
get :index
93+
assert_select "table" do |table|
94+
assert_no_match "20130906111511", table.text
95+
end
96+
assert_select ".flash", text: "Migration 20130906111511 was successfully deleted."
97+
assert_nil ActiveRecord::Base.connection.select_value(sql)
98+
end
99+
100+
test "POST #delete_all removes all broken migration entries from the schema_migrations table" do
101+
delete_migrations_files
102+
sql = "SELECT COUNT(*) FROM schema_migrations"
103+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
104+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
105+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
106+
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
107+
108+
post :delete_all
109+
assert_response :redirect
110+
get :index
111+
assert_select "p", text: "No broken versions found."
112+
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
113+
assert_equal 0, ActiveRecord::Base.connection.select_value(sql)
114+
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
115+
assert_equal 0, ActiveRecord::Base.connection.select_value(sql)
116+
end
117+
end
118+
end

test/controllers/actual_db_schema/migrations_controller_test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def routes_setup
2121
@routes = @app.routes
2222
Rails.application.routes.draw do
2323
get "/rails/phantom_migrations" => "actual_db_schema/phantom_migrations#index", as: "phantom_migrations"
24+
get "/rails/broken_versions" => "actual_db_schema/broken_versions#index", as: "broken_versions"
2425
get "/rails/schema" => "actual_db_schema/schema#index", as: "schema"
2526
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
2627
get "/rails/migration/:id" => "actual_db_schema/migrations#show", as: "migration"

test/support/test_utils.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ def delete_migrations_files(prefix_name = nil)
6262
delete_migrations_files_for(path)
6363
end
6464

65+
def delete_migrations_files_for(path)
66+
Dir.glob(app_file("#{path}/**/*.rb")).each do |file|
67+
remove_app_dir(file)
68+
end
69+
end
70+
6571
def define_migration_file(filename, content, prefix: nil)
6672
path =
6773
case prefix
@@ -200,12 +206,6 @@ def schema_migration_class
200206
end
201207
end
202208

203-
def delete_migrations_files_for(path)
204-
Dir.glob(app_file("#{path}/**/*.rb")).each do |file|
205-
remove_app_dir(file)
206-
end
207-
end
208-
209209
def migrated_files_call(prefix_name = nil)
210210
migrated_path = ActualDbSchema.config[:migrated_folder].presence || migrated_paths.first
211211
path = MIGRATED_PATHS.fetch(prefix_name&.to_sym, migrated_path.to_s)

0 commit comments

Comments
 (0)