discourse/lib/migration/column_dropper.rb
Sam 6a3c8fe69c 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
2018-03-21 15:43:32 +11:00

65 lines
2.0 KiB
Ruby

require_dependency 'migration/base_dropper'
module Migration
class ColumnDropper < BaseDropper
def self.drop(table:, after_migration:, columns:, delay: nil, on_drop: nil)
validate_table_name(table)
columns.each { |column| validate_column_name(column) }
ColumnDropper.new(table, columns, after_migration, delay, on_drop).delayed_drop
end
def self.mark_readonly(table_name, column_name)
create_readonly_function(table_name, column_name)
ActiveRecord::Base.exec_sql <<~SQL
CREATE TRIGGER #{readonly_trigger_name(table_name, column_name)}
BEFORE INSERT OR UPDATE OF #{column_name}
ON #{table_name}
FOR EACH ROW
WHEN (NEW.#{column_name} IS NOT NULL)
EXECUTE PROCEDURE #{readonly_function_name(table_name, column_name)};
SQL
end
private
def initialize(table, columns, after_migration, delay, on_drop)
super(after_migration, delay, on_drop)
@table = table
@columns = columns
end
def droppable?
builder = SqlBuilder.new(<<~SQL)
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
/*where*/
LIMIT 1
SQL
builder.where("table_schema = 'public'")
.where("table_name = :table")
.where("column_name IN (:columns)")
.where(previous_migration_done)
.exec(table: @table,
columns: @columns,
delay: "#{@delay} seconds",
after_migration: @after_migration).to_a.length > 0
end
def execute_drop!
@columns.each do |column|
ActiveRecord::Base.exec_sql <<~SQL
DROP TRIGGER IF EXISTS #{BaseDropper.readonly_trigger_name(@table, column)} ON #{@table};
DROP FUNCTION IF EXISTS #{BaseDropper.readonly_function_name(@table, column)} CASCADE;
SQL
# safe cause it is protected on method entry, can not be passed in params
ActiveRecord::Base.exec_sql("ALTER TABLE #{@table} DROP COLUMN IF EXISTS #{column}")
end
end
end
end