mirror of
https://github.com/discourse/discourse.git
synced 2025-05-31 17:45:54 +08:00
FEATURE: protect against accidental column or table drops
Often we need to amend our schema, it is tempting to use drop_table, rename_column and drop_column to amned schema trouble though is that existing code that is running in production can depend on the existance of previous schema leading to application breaking until new code base is deployed. The commit enforces new rules to ensure we can never drop tables or columns in migrations and instead use Migration::ColumnDropper and Migration::TableDropper to defer drop the db objects
This commit is contained in:
69
lib/migration/table_dropper.rb
Normal file
69
lib/migration/table_dropper.rb
Normal file
@ -0,0 +1,69 @@
|
||||
require_dependency 'migration/base_dropper'
|
||||
|
||||
module Migration
|
||||
class Migration::TableDropper < BaseDropper
|
||||
def self.delayed_drop(old_name:, new_name:, after_migration:, delay: nil, on_drop: nil)
|
||||
validate_table_name(old_name)
|
||||
validate_table_name(new_name)
|
||||
|
||||
TableDropper.new(old_name, new_name, after_migration, delay, on_drop).delayed_drop
|
||||
end
|
||||
|
||||
def self.read_only_table(table_name)
|
||||
create_readonly_function(table_name)
|
||||
|
||||
ActiveRecord::Base.exec_sql <<~SQL
|
||||
CREATE TRIGGER #{readonly_trigger_name(table_name)}
|
||||
BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE
|
||||
ON #{table_name}
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE PROCEDURE #{readonly_function_name(table_name)};
|
||||
SQL
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(old_name, new_name, after_migration, delay, on_drop)
|
||||
super(after_migration, delay, on_drop)
|
||||
|
||||
@old_name = old_name
|
||||
@new_name = new_name
|
||||
end
|
||||
|
||||
def droppable?
|
||||
builder = SqlBuilder.new(<<~SQL)
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
/*where*/
|
||||
LIMIT 1
|
||||
SQL
|
||||
|
||||
builder.where("table_schema = 'public'")
|
||||
.where(previous_migration_done)
|
||||
.where(new_table_exists)
|
||||
.exec(old_name: @old_name,
|
||||
new_name: @new_name,
|
||||
delay: "#{@delay} seconds",
|
||||
after_migration: @after_migration).to_a.length > 0
|
||||
end
|
||||
|
||||
def new_table_exists
|
||||
<<~SQL
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE table_schema = 'public' AND
|
||||
table_name = :new_name
|
||||
)
|
||||
SQL
|
||||
end
|
||||
|
||||
def execute_drop!
|
||||
ActiveRecord::Base.exec_sql("DROP TABLE IF EXISTS #{@old_name}")
|
||||
|
||||
ActiveRecord::Base.exec_sql <<~SQL
|
||||
DROP FUNCTION IF EXISTS #{BaseDropper.readonly_function_name(@old_name)} CASCADE;
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user