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

Make rollback more informative #104

Merged
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
1 change: 1 addition & 0 deletions lib/actual_db_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_relative "actual_db_schema/migration"
require_relative "actual_db_schema/failed_migration"
require_relative "actual_db_schema/migration_context"
require_relative "actual_db_schema/output_formatter"
require_relative "actual_db_schema/patches/migration_proxy"
require_relative "actual_db_schema/patches/migrator"
require_relative "actual_db_schema/patches/migration_context"
Expand Down
78 changes: 44 additions & 34 deletions lib/actual_db_schema/commands/rollback.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ module ActualDbSchema
module Commands
# Rolls back all phantom migrations
class Rollback < Base
UNICODE_COLORS = {
red: 31,
green: 32,
yellow: 33
}.freeze
include ActualDbSchema::OutputFormatter
include ActionView::Helpers::TextHelper

def initialize(context, manual_mode: false)
@manual_mode = manual_mode || manual_mode_default?
Expand All @@ -18,50 +15,63 @@ def initialize(context, manual_mode: false)
private

def call_impl
context.rollback_branches(manual_mode: @manual_mode)
rolled_back = context.rollback_branches(manual_mode: @manual_mode)

return if ActualDbSchema.failed.empty?
return unless rolled_back

puts_preamble
puts_into
puts ""
puts failed_migrations_list
puts_preamble
ActualDbSchema.failed.empty? ? print_success : print_error
end

def print_success
puts colorize("[ActualDbSchema] All phantom migrations rolled back successfully! 🎉", :green)
end

def print_error
header_message = <<~HEADER
#{ActualDbSchema.failed.count} phantom migration(s) could not be rolled back automatically.

Try these steps to fix and move forward:
1. Ensure the migrations are reversible (define #up and #down methods or use #reversible).
2. If the migration references code or tables from another branch, restore or remove them.
3. Once fixed, run `rails db:migrate` again.

Below are the details of the problematic migrations:
HEADER

print_error_summary("#{header_message}\n#{failed_migrations_list}")
end

def failed_migrations_list
ActualDbSchema.failed.map.with_index(1) do |failed, index|
filename = failed.short_filename
exception = failed.exception
<<~MSG
\t#{colorize("[Migration##{index}]", :yellow)}
\t- #{filename}

\t\t#{exception.inspect.gsub("\n", "\n\t ")}
MSG
end
<<~MIGRATION
#{colorize("Migration ##{index}:", :yellow)}
File: #{failed.short_filename}
Branch: #{failed.branch}
MIGRATION
end.join("\n")
end

def puts_preamble
puts ""
puts %(\u2757\u2757\u2757 #{colorize("[ActualDbSchema]", :red)})
puts ""
def print_error_summary(content)
width = 100
indent = 4
gem_name = "ActualDbSchema"

puts colorize("╔═ [#{gem_name}] #{"═" * (width - gem_name.length - 5)}╗", :red)
print_wrapped_content(content, width, indent)
puts colorize("╚#{"═" * width}╝", :red)
end

def puts_into
msg = "#{ActualDbSchema.failed.count} phantom migration(s) could not be rolled back automatically."
msg += " Roll them back or fix manually:"
puts colorize(msg, :red)
def print_wrapped_content(content, width, indent)
usable_width = width - indent - 4
wrapped_content = word_wrap(content, line_width: usable_width)
wrapped_content.each_line do |line|
puts "#{" " * indent}#{line.chomp}"
end
end

def manual_mode_default?
ActualDbSchema.config[:auto_rollback_disabled]
end

def colorize(text, color)
code = UNICODE_COLORS.fetch(color, 37)
"\e[#{code}m#{text}\e[0m"
end
end
end
end
2 changes: 1 addition & 1 deletion lib/actual_db_schema/failed_migration.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module ActualDbSchema
FailedMigration = Struct.new(:migration, :exception, keyword_init: true) do
FailedMigration = Struct.new(:migration, :exception, :branch, keyword_init: true) do
def filename
migration.filename
end
Expand Down
18 changes: 18 additions & 0 deletions lib/actual_db_schema/output_formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module ActualDbSchema
# Provides functionality for formatting terminal output with colors
module OutputFormatter
UNICODE_COLORS = {
red: 31,
green: 32,
yellow: 33,
gray: 90
}.freeze

def colorize(text, color)
code = UNICODE_COLORS.fetch(color, 37)
"\e[#{code}m#{text}\e[0m"
end
end
end
30 changes: 28 additions & 2 deletions lib/actual_db_schema/patches/migration_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ module ActualDbSchema
module Patches
# Add new command to roll back the phantom migrations
module MigrationContext
include ActualDbSchema::OutputFormatter

def rollback_branches(manual_mode: false)
rolled_back = false

phantom_migrations.reverse_each do |migration|
next unless status_up?(migration)

rolled_back = true
show_info_for(migration) if manual_mode
migrate(migration) if !manual_mode || user_wants_rollback?
rescue StandardError => e
ActualDbSchema.failed << FailedMigration.new(migration: migration, exception: e)
handle_rollback_error(migration, e)
end

rolled_back
end

def phantom_migrations
Expand Down Expand Up @@ -63,7 +70,7 @@ def user_wants_rollback?
end

def show_info_for(migration)
puts "\n[ActualDbSchema] A phantom migration was found and is about to be rolled back."
puts colorize("\n[ActualDbSchema] A phantom migration was found and is about to be rolled back.", :gray)
puts "Please make a decision from the options below to proceed.\n\n"
puts "Branch: #{branch_for(migration.version.to_s)}"
puts "Database: #{ActualDbSchema.db_config[:database]}"
Expand All @@ -72,6 +79,10 @@ def show_info_for(migration)
end

def migrate(migration)
message = "[ActualDbSchema] Rolling back phantom migration #{migration.version} #{migration.name} " \
"(from branch: #{branch_for(migration.version.to_s)})"
puts colorize(message, :gray)

migrator = down_migrator_for(migration)
migrator.extend(ActualDbSchema::Patches::Migrator)
migrator.migrate
Expand All @@ -84,6 +95,21 @@ def branch_for(version)
def metadata
@metadata ||= ActualDbSchema::Store.instance.read
end

def handle_rollback_error(migration, exception)
error_message = <<~ERROR
Error encountered during rollback:

#{exception.message.gsub(/^An error has occurred, all later migrations canceled:\s*/, "").strip}
ERROR

puts colorize(error_message, :red)
ActualDbSchema.failed << FailedMigration.new(
migration: migration,
exception: exception,
branch: branch_for(migration.version.to_s)
)
end
end
end
end
6 changes: 6 additions & 0 deletions test/rake_task_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
assert_empty TestingState.down
utils.run_migrations
assert_equal %i[second first], TestingState.down
assert_match(/\[ActualDbSchema\] Rolling back phantom migration/, TestingState.output)
end

describe "with irreversible migration" do
Expand All @@ -59,6 +60,8 @@ def down
assert_empty ActualDbSchema.failed
utils.run_migrations
assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })
assert_match(/Error encountered during rollback:/, TestingState.output)
assert_match(/ActiveRecord::IrreversibleMigration/, TestingState.output)
end
end

Expand All @@ -84,6 +87,9 @@ def down
utils.run_migrations
assert_equal(%w[20130906111510_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })
assert_match(/1 phantom migration\(s\) could not be rolled back automatically/, TestingState.output)
assert_match(/Try these steps to fix and move forward:/, TestingState.output)
assert_match(/Below are the details of the problematic migrations:/, TestingState.output)
assert_match(%r{File: tmp/migrated/20130906111510_irreversible.rb}, TestingState.output)
end
end
end
Expand Down
Loading