mirror of
https://github.com/discourse/discourse.git
synced 2025-05-24 03:36:18 +08:00
DEV: Protection for migrations that creates index concurrently take 2 (#31792)
This is a follow up to 6820622467ab3613e824f0cb6219def2a575bc1d. This commit addresses migrations that uses `remove_index` with the `if_exists: true` option to drop an existing index before creating an index using the `concurrently` option. This commit also reruns two migration which may have caused indexes to be left in an `invalid` state. ### Reviewers Note Plugin tests are failing due to https://github.com/discourse/discourse-translator/pull/251
This commit is contained in:

committed by
GitHub

parent
0ebd0a0bd5
commit
02b8aa6096
@ -1,9 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class AddIndexToUsersIpAddress < ActiveRecord::Migration[7.1]
|
|
||||||
disable_ddl_transaction!
|
|
||||||
|
|
||||||
def change
|
|
||||||
add_index :users, :ip_address, algorithm: :concurrently, name: "idx_users_ip_address"
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,10 +1,21 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class RemoveUniqueConstraintFromTagUsersIndexes < ActiveRecord::Migration[7.1]
|
class RemoveUniqueConstraintFromTagUsersIndexes < ActiveRecord::Migration[7.1]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
remove_index :tag_users, name: :idx_tag_users_ix1, algorithm: :concurrently
|
remove_index :tag_users, name: :idx_tag_users_ix1, algorithm: :concurrently, if_exists: true
|
||||||
remove_index :tag_users, name: :idx_tag_users_ix2, algorithm: :concurrently
|
remove_index :tag_users, name: :idx_tag_users_ix2, algorithm: :concurrently, if_exists: true
|
||||||
|
|
||||||
|
remove_index :tag_users,
|
||||||
|
%i[user_id tag_id notification_level],
|
||||||
|
algorithm: :concurrently,
|
||||||
|
if_exists: true
|
||||||
|
|
||||||
|
remove_index :tag_users,
|
||||||
|
%i[tag_id user_id notification_level],
|
||||||
|
algorithm: :concurrently,
|
||||||
|
if_exists: true
|
||||||
|
|
||||||
add_index :tag_users, %i[user_id tag_id notification_level], algorithm: :concurrently
|
add_index :tag_users, %i[user_id tag_id notification_level], algorithm: :concurrently
|
||||||
add_index :tag_users, %i[tag_id user_id notification_level], algorithm: :concurrently
|
add_index :tag_users, %i[tag_id user_id notification_level], algorithm: :concurrently
|
19
db/migrate/20250313045010_add_index_to_users_ip_address.rb
Normal file
19
db/migrate/20250313045010_add_index_to_users_ip_address.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexToUsersIpAddress < ActiveRecord::Migration[7.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
remove_index :users,
|
||||||
|
:ip_address,
|
||||||
|
algorithm: :concurrently,
|
||||||
|
name: "idx_users_ip_address",
|
||||||
|
if_exists: true
|
||||||
|
|
||||||
|
add_index :users, :ip_address, algorithm: :concurrently, name: "idx_users_ip_address"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
@ -78,6 +78,20 @@ class Migration::SafeMigrate
|
|||||||
return if ENV["RAILS_ENV"] == "production"
|
return if ENV["RAILS_ENV"] == "production"
|
||||||
|
|
||||||
@@migration_sqls = []
|
@@migration_sqls = []
|
||||||
|
@@activerecord_remove_indexes = []
|
||||||
|
|
||||||
|
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
|
||||||
|
alias_method :original_remove_index, :remove_index
|
||||||
|
|
||||||
|
def remove_index(table_name, column_name = nil, **options)
|
||||||
|
@@activerecord_remove_indexes << (
|
||||||
|
options[:name] || index_name(table_name, options.merge(column: column_name))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call the original method
|
||||||
|
original_remove_index(table_name, column_name, **options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
PG::Connection.class_eval do
|
PG::Connection.class_eval do
|
||||||
alias_method :exec_migrator_unpatched, :exec
|
alias_method :exec_migrator_unpatched, :exec
|
||||||
@ -99,7 +113,13 @@ class Migration::SafeMigrate
|
|||||||
return if !PG::Connection.method_defined?(:exec_migrator_unpatched)
|
return if !PG::Connection.method_defined?(:exec_migrator_unpatched)
|
||||||
return if ENV["RAILS_ENV"] == "production"
|
return if ENV["RAILS_ENV"] == "production"
|
||||||
|
|
||||||
@@migration_sqls = []
|
@@migration_sqls.clear
|
||||||
|
@@activerecord_remove_indexes.clear
|
||||||
|
|
||||||
|
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
|
||||||
|
alias_method :remove_index, :original_remove_index
|
||||||
|
remove_method :original_remove_index
|
||||||
|
end
|
||||||
|
|
||||||
PG::Connection.class_eval do
|
PG::Connection.class_eval do
|
||||||
alias_method :exec, :exec_migrator_unpatched
|
alias_method :exec, :exec_migrator_unpatched
|
||||||
@ -120,21 +140,6 @@ class Migration::SafeMigrate
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
UNSAFE_DROP_INDEX_CONCURRENTLY_WARNING = <<~TEXT
|
|
||||||
WARNING
|
|
||||||
-------------------------------------------------------------------------------------
|
|
||||||
An attempt was made to create an index concurrently in a migration without first dropping the index.
|
|
||||||
|
|
||||||
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 .
|
|
||||||
|
|
||||||
Please update the migration to first drop the index if it exists before creating it concurrently.
|
|
||||||
TEXT
|
|
||||||
|
|
||||||
def self.protect!(sql)
|
def self.protect!(sql)
|
||||||
@@migration_sqls << sql
|
@@migration_sqls << sql
|
||||||
|
|
||||||
@ -169,15 +174,37 @@ class Migration::SafeMigrate
|
|||||||
TEXT
|
TEXT
|
||||||
raise Discourse::InvalidMigration, "Attempt was made to rename or delete column"
|
raise Discourse::InvalidMigration, "Attempt was made to rename or delete column"
|
||||||
elsif sql =~ /\A\s*create\s+(?:unique\s+)?index\s+concurrently\s+/i
|
elsif sql =~ /\A\s*create\s+(?:unique\s+)?index\s+concurrently\s+/i
|
||||||
|
index_name =
|
||||||
|
sql.match(/\bINDEX\s+CONCURRENTLY\s+(?:IF\s+NOT\s+EXISTS\s+)?"?([a-zA-Z0-9_\.]+)"?/i)[1]
|
||||||
|
|
||||||
|
return if @@activerecord_remove_indexes.include?(index_name)
|
||||||
|
|
||||||
match = sql.match(/\bON\s+(?:ONLY\s+)?(?:"([^"]+)"|([a-zA-Z0-9_\.]+))/i)
|
match = sql.match(/\bON\s+(?:ONLY\s+)?(?:"([^"]+)"|([a-zA-Z0-9_\.]+))/i)
|
||||||
table_name = match[1] || match[2]
|
table_name = match[1] || match[2]
|
||||||
|
|
||||||
if !@@migration_sqls.any? { |migration_sql|
|
has_drop_index_statement =
|
||||||
migration_sql =~ /\A\s*drop\s+index\s+(?:concurrently\s+)?if\s+exists\s+/i &&
|
@@migration_sqls.any? do |migration_sql|
|
||||||
migration_sql.include?(table_name)
|
migration_sql =~ /\A\s*drop\s+index/i && migration_sql.include?(table_name) &&
|
||||||
}
|
migration_sql.include?(index_name)
|
||||||
$stdout.puts("", UNSAFE_DROP_INDEX_CONCURRENTLY_WARNING)
|
end
|
||||||
end
|
|
||||||
|
return if has_drop_index_statement
|
||||||
|
|
||||||
|
raise(Discourse::InvalidMigration, <<~RAW)
|
||||||
|
WARNING
|
||||||
|
-------------------------------------------------------------------------------------
|
||||||
|
An attempt was made to create an index concurrently in a migration without first dropping the index.
|
||||||
|
SQL used was: '#{sql}'
|
||||||
|
|
||||||
|
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 .
|
||||||
|
|
||||||
|
Please update the migration to first drop the index if it exists before creating it concurrently.
|
||||||
|
RAW
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -11,10 +11,6 @@ class CreateIndexConcurrently < ActiveRecord::Migration[7.2]
|
|||||||
execute <<~SQL
|
execute <<~SQL
|
||||||
CREATE INDEX CONCURRENTLY some_notifications_index ON notifications(user_id);
|
CREATE INDEX CONCURRENTLY some_notifications_index ON notifications(user_id);
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
execute <<~SQL
|
|
||||||
DROP INDEX some_notifications_index;
|
|
||||||
SQL
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def down
|
def down
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SomeMigration < ActiveRecord::Migration[7.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
remove_index :notifications, %i[user_id id], if_exists: true
|
||||||
|
add_index :notifications, %i[user_id id], algorithm: :concurrently
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise "not tested"
|
||||||
|
end
|
||||||
|
end
|
@ -71,11 +71,44 @@ RSpec.describe Migration::SafeMigrate do
|
|||||||
expect { User.first.username }.not_to raise_error
|
expect { User.first.username }.not_to raise_error
|
||||||
end
|
end
|
||||||
|
|
||||||
it "allows running a migration that creates an index concurrently if it drops the index first" do
|
it "allows running a migration that creates an index concurrently if it checks if the index exists first" do
|
||||||
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/create_index_concurrently_safe"
|
Migration::SafeMigrate.enable!
|
||||||
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)
|
path =
|
||||||
|
File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/create_index_concurrently_safe_activerecord"
|
||||||
|
|
||||||
|
error = nil
|
||||||
|
|
||||||
|
capture_stdout do
|
||||||
|
begin
|
||||||
|
migrate_up(path)
|
||||||
|
rescue StandardError => e
|
||||||
|
error = e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(error.cause.cause.message).to include(
|
||||||
|
"CREATE INDEX CONCURRENTLY cannot run inside a transaction block",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows running a migration that creates an index concurrently if it drops the index first" do
|
||||||
|
Migration::SafeMigrate.enable!
|
||||||
|
|
||||||
|
path = File.expand_path "#{Rails.root}/spec/fixtures/db/migrate/create_index_concurrently_safe"
|
||||||
|
error = nil
|
||||||
|
|
||||||
|
capture_stdout do
|
||||||
|
begin
|
||||||
|
migrate_up(path)
|
||||||
|
rescue StandardError => e
|
||||||
|
error = e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(error.cause.cause.message).to include(
|
||||||
|
"CREATE INDEX CONCURRENTLY cannot run inside a transaction block",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "bans running a migration that creates an index concurrently without first dropping the index if it exists" do
|
it "bans running a migration that creates an index concurrently without first dropping the index if it exists" do
|
||||||
@ -84,9 +117,17 @@ RSpec.describe Migration::SafeMigrate do
|
|||||||
path =
|
path =
|
||||||
File.expand_path("#{Rails.root}/spec/fixtures/db/migrate/create_index_concurrently_unsafe")
|
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) }
|
error = nil
|
||||||
|
|
||||||
expect(output).to include(described_class::UNSAFE_DROP_INDEX_CONCURRENTLY_WARNING)
|
capture_stdout do
|
||||||
|
migrate_up(path)
|
||||||
|
rescue StandardError => e
|
||||||
|
error = e
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(error.message).to include(
|
||||||
|
"An attempt was made to create an index concurrently in a migration without first dropping the index.",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "allows dropping NOT NULL" do
|
it "allows dropping NOT NULL" do
|
||||||
|
Reference in New Issue
Block a user