2017-05-04 10:15:32 -04:00
|
|
|
class ColumnDropper
|
2017-05-04 13:41:05 -04:00
|
|
|
def self.drop(table:, after_migration:, columns:, delay: nil, on_drop: nil)
|
2017-05-04 10:15:32 -04:00
|
|
|
raise ArgumentError.new("Invalid table name passed to drop #{table}") if table =~ /[^a-z0-9_]/i
|
|
|
|
|
|
|
|
columns.each do |column|
|
|
|
|
raise ArgumentError.new("Invalid column name passed to drop #{column}") if column =~ /[^a-z0-9_]/i
|
|
|
|
end
|
|
|
|
|
2018-02-27 17:21:38 -05:00
|
|
|
# in production we need some extra delay to allow for slow migrations
|
2018-02-27 17:35:46 -05:00
|
|
|
delay ||= Rails.env.production? ? 3600 : 0
|
2017-05-04 10:15:32 -04:00
|
|
|
|
2017-04-26 14:47:36 -04:00
|
|
|
sql = <<~SQL
|
2017-05-04 10:15:32 -04:00
|
|
|
SELECT 1
|
|
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
|
|
WHERE table_schema = 'public' AND
|
2017-04-26 14:47:36 -04:00
|
|
|
table_name = :table AND
|
|
|
|
column_name IN (:columns) AND
|
|
|
|
EXISTS (
|
|
|
|
SELECT 1
|
|
|
|
FROM schema_migration_details
|
|
|
|
WHERE name = :after_migration AND
|
|
|
|
created_at <= (current_timestamp at time zone 'UTC' - interval :delay)
|
|
|
|
)
|
2017-05-04 10:15:32 -04:00
|
|
|
LIMIT 1
|
2017-04-26 14:47:36 -04:00
|
|
|
SQL
|
2017-05-04 10:15:32 -04:00
|
|
|
|
|
|
|
if ActiveRecord::Base.exec_sql(sql, table: table,
|
|
|
|
columns: columns,
|
2018-02-27 17:21:38 -05:00
|
|
|
delay: "#{delay.to_i || 0} seconds",
|
2017-05-04 10:15:32 -04:00
|
|
|
after_migration: after_migration).to_a.length > 0
|
2017-04-26 14:47:36 -04:00
|
|
|
on_drop&.call
|
2017-05-04 10:15:32 -04:00
|
|
|
|
2017-04-26 14:47:36 -04:00
|
|
|
columns.each do |column|
|
|
|
|
ActiveRecord::Base.exec_sql <<~SQL
|
2017-08-30 03:33:25 -04:00
|
|
|
DROP TRIGGER IF EXISTS #{readonly_trigger_name(table, column)} ON #{table};
|
2017-08-30 03:54:27 -04:00
|
|
|
DROP FUNCTION IF EXISTS #{readonly_function_name(table, column)} CASCADE;
|
2017-04-26 14:47:36 -04:00
|
|
|
SQL
|
2017-08-29 11:50:56 -04:00
|
|
|
|
|
|
|
# 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}")
|
2017-04-26 14:47:36 -04:00
|
|
|
end
|
2017-07-25 02:36:30 -04:00
|
|
|
|
|
|
|
Discourse.reset_active_record_cache
|
2017-05-04 10:15:32 -04:00
|
|
|
end
|
|
|
|
end
|
2017-04-26 14:47:36 -04:00
|
|
|
|
|
|
|
def self.mark_readonly(table_name, column_name)
|
|
|
|
ActiveRecord::Base.exec_sql <<-SQL
|
|
|
|
CREATE OR REPLACE FUNCTION #{readonly_function_name(table_name, column_name)} RETURNS trigger AS $rcr$
|
|
|
|
BEGIN
|
|
|
|
RAISE EXCEPTION 'Discourse: #{column_name} in #{table_name} is readonly';
|
|
|
|
END
|
|
|
|
$rcr$ LANGUAGE plpgsql;
|
|
|
|
SQL
|
|
|
|
|
|
|
|
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 self.readonly_function_name(table_name, column_name)
|
|
|
|
"raise_#{table_name}_#{column_name}_readonly()"
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.readonly_trigger_name(table_name, column_name)
|
|
|
|
"#{table_name}_#{column_name}_readonly"
|
|
|
|
end
|
2017-05-04 10:15:32 -04:00
|
|
|
end
|