discourse/lib/migration/base_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

73 lines
2.0 KiB
Ruby

module Migration
class BaseDropper
def initialize(after_migration, delay, on_drop)
@after_migration = after_migration
@on_drop = on_drop
# in production we need some extra delay to allow for slow migrations
@delay = delay || (Rails.env.production? ? 3600 : 0)
end
def delayed_drop
if droppable?
@on_drop&.call
execute_drop!
Discourse.reset_active_record_cache
end
end
private
def droppable?
raise NotImplementedError
end
def execute_drop!
raise NotImplementedError
end
def previous_migration_done
<<~SQL
EXISTS(
SELECT 1
FROM schema_migration_details
WHERE name = :after_migration AND
created_at <= (current_timestamp AT TIME ZONE 'UTC' - INTERVAL :delay)
)
SQL
end
def self.create_readonly_function(table_name, column_name = nil)
message = column_name ?
"Discourse: #{column_name} in #{table_name} is readonly" :
"Discourse: #{table_name} is read only"
ActiveRecord::Base.exec_sql <<~SQL
CREATE OR REPLACE FUNCTION #{readonly_function_name(table_name, column_name)} RETURNS trigger AS $rcr$
BEGIN
RAISE EXCEPTION '#{message}';
END
$rcr$ LANGUAGE plpgsql;
SQL
end
private_class_method :create_readonly_function
def self.validate_table_name(table_name)
raise ArgumentError.new("Invalid table name passed: #{table_name}") if table_name =~ /[^a-z0-9_]/i
end
def self.validate_column_name(column_name)
raise ArgumentError.new("Invalid column name passed to drop #{column_name}") if column_name =~ /[^a-z0-9_]/i
end
def self.readonly_function_name(table_name, column_name = nil)
["raise", table_name, column_name, "readonly()"].compact.join("_")
end
def self.readonly_trigger_name(table_name, column_name = nil)
[table_name, column_name, "readonly"].compact.join("_")
end
end
end