Files
discourse/spec/lib/migration/safe_migrate_spec.rb
Alan Guo Xiang Tan 6820622467 DEV: Add protection for migrations that creates index concurrently (#31763)
This commit updates `Migration::SafeMigrate` to protect against unsafe
ways of adding a Postgres index concurrently.

Per postgres documentation:

If a problem arises while scanning the table, such as a deadlock or a
uniqueness violation in a unique index,
the CREATE INDEX command will fail but leave behind an "invalid" index.
This index will be ignored for querying
purposes because it might be incomplete; however it will still consume
update overhead. The recommended recovery
method in such cases is to drop the index and try again to perform
CREATE INDEX CONCURRENTLY .

Therefore, the simplest way for us to ensure that migrations that create
indexes concurrently are idempotent is to follow postgres'
recommendation of dropping the index first before trying to create the
index concurrently.
2025-03-13 09:34:14 +08:00

135 lines
4.2 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Migration::SafeMigrate do
before { Migration::SafeMigrate::SafeMigration.disable_safe! }
after do
Migration::SafeMigrate.disable!
Migration::SafeMigrate::SafeMigration.enable_safe!
end
def migrate_up(path)
migrations = ActiveRecord::MigrationContext.new(path).migrations
ActiveRecord::Migrator.new(
:up,
migrations,
ActiveRecord::Base.connection_pool.schema_migration,
ActiveRecord::Base.connection_pool.internal_metadata,
migrations.first.version,
).run
end
it "bans all table removal" do
Migration::SafeMigrate.enable!
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/drop_table"
output = capture_stdout { expect do migrate_up(path) end.to raise_error(StandardError) }
expect(output).to include("rails g post_migration")
expect { User.first }.not_to raise_error
expect(User.first).not_to eq(nil)
end
it "bans all table renames" do
Migration::SafeMigrate.enable!
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/rename_table"
output = capture_stdout { expect do migrate_up(path) end.to raise_error(StandardError) }
expect { User.first }.not_to raise_error
expect(User.first).not_to eq(nil)
expect(output).to include("rails g post_migration")
end
it "bans all column removal" do
Migration::SafeMigrate.enable!
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/remove_column"
output = capture_stdout { expect do migrate_up(path) end.to raise_error(StandardError) }
expect(output).to include("rails g post_migration")
expect(User.first).not_to eq(nil)
expect { User.first.username }.not_to raise_error
end
it "bans all column renames" do
Migration::SafeMigrate.enable!
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/rename_column"
output = capture_stdout { expect do migrate_up(path) end.to raise_error(StandardError) }
expect(output).to include("rails g post_migration")
expect(User.first).not_to eq(nil)
expect { User.first.username }.not_to raise_error
end
it "allows running a migration that creates an index concurrently if it drops the index first" do
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/create_index_concurrently_safe"
output = capture_stdout { expect do migrate_up(path) end.to raise_error(StandardError) }
expect(output).not_to include(described_class::UNSAFE_DROP_INDEX_CONCURRENTLY_WARNING)
end
it "bans running a migration that creates an index concurrently without first dropping the index if it exists" do
Migration::SafeMigrate.enable!
path =
File.expand_path("#{Rails.root}/spec/fixtures/db/migrate/create_index_concurrently_unsafe")
output = capture_stdout { expect do migrate_up(path) end.to raise_error(StandardError) }
expect(output).to include(described_class::UNSAFE_DROP_INDEX_CONCURRENTLY_WARNING)
end
it "allows dropping NOT NULL" do
Migration::SafeMigrate.enable!
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/drop_not_null"
output = capture_stdout { migrate_up(path) }
expect(output).to include("change_column_null(:users, :username, true, nil)")
end
it "supports being disabled" do
Migration::SafeMigrate.enable!
Migration::SafeMigrate.disable!
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/drop_table"
output = capture_stdout { migrate_up(path) }
expect(output).to include("drop_table(:email_logs)")
end
describe "for a post deployment migration" do
it "should not ban unsafe migrations using up" do
Migration::SafeMigrate::SafeMigration.enable_safe!
path = File.expand_path "#{Rails.root}/spec/fixtures/db/post_migrate/drop_table"
output = capture_stdout { migrate_up(path) }
expect(output).to include("drop_table(:email_logs)")
end
it "should not ban unsafe migrations using change" do
Migration::SafeMigrate::SafeMigration.enable_safe!
path = File.expand_path "#{Rails.root}/spec/fixtures/db/post_migrate/change"
output = capture_stdout { migrate_up(path) }
expect(output).to include("drop_table(:email_logs)")
end
end
end