diff --git a/db/fixtures/999_delayed.rb b/db/fixtures/999_delayed.rb new file mode 100644 index 00000000000..1141e13e64a --- /dev/null +++ b/db/fixtures/999_delayed.rb @@ -0,0 +1,12 @@ +# Delayed migration steps + +require 'table_migration_helper' + +TableMigrationHelper.delayed_drop( + old_name: 'topic_status_updates', + new_name: 'topic_timers', + after_migration: 'RenameTopicStatusUpdatesToTopicTimers', + on_drop: ->(){ + STDERR.puts "Dropping topic_status_updates. It was moved to topic_timers." + } +) diff --git a/db/migrate/20170512185227_create_topic_status_updates_again.rb b/db/migrate/20170512185227_create_topic_status_updates_again.rb new file mode 100644 index 00000000000..aaa8ba70df2 --- /dev/null +++ b/db/migrate/20170512185227_create_topic_status_updates_again.rb @@ -0,0 +1,23 @@ +require 'table_migration_helper' + +class CreateTopicStatusUpdatesAgain < ActiveRecord::Migration + def up + create_table :topic_status_updates do |t| + t.datetime :execute_at, null: false + t.integer :status_type, null: false + t.integer :user_id, null: false + t.integer :topic_id, null: false + t.boolean :based_on_last_post, null: false, default: false + t.datetime :deleted_at + t.integer :deleted_by_id + t.timestamps + t.integer :category_id + end + + TableMigrationHelper.read_only_table('topic_status_updates') + end + + def down + drop_table :topic_status_updates + end +end diff --git a/lib/table_migration_helper.rb b/lib/table_migration_helper.rb new file mode 100644 index 00000000000..6268626bb3f --- /dev/null +++ b/lib/table_migration_helper.rb @@ -0,0 +1,53 @@ +class TableMigrationHelper + def self.read_only_table(table_name) + ActiveRecord::Base.exec_sql <<-SQL + CREATE OR REPLACE FUNCTION raise_read_only() RETURNS trigger AS $rro$ + BEGIN + RAISE EXCEPTION 'Table is read only'; + RETURN null; + END + $rro$ LANGUAGE plpgsql; + SQL + + ActiveRecord::Base.exec_sql <<-SQL + CREATE TRIGGER #{table_name}_read_only + BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE + ON #{table_name} + FOR EACH STATEMENT + EXECUTE PROCEDURE raise_read_only(); + SQL + end + + def self.delayed_drop(old_name:, new_name:, after_migration:, delay: nil, on_drop: nil) + delay ||= Rails.env.production? ? 300 : 0 + + sql = < 0 + on_drop&.call + + ActiveRecord::Base.exec_sql("DROP TABLE #{old_name}") + end + end +end diff --git a/spec/components/table_migration_helper_spec.rb b/spec/components/table_migration_helper_spec.rb new file mode 100644 index 00000000000..12aa17614b9 --- /dev/null +++ b/spec/components/table_migration_helper_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' +require 'table_migration_helper' + +describe TableMigrationHelper do + + def table_exists?(table_name) + sql = <<-SQL + SELECT 1 + FROM INFORMATION_SCHEMA.TABLES + WHERE table_schema = 'public' AND + table_name = '#{table_name}' + SQL + + ActiveRecord::Base.exec_sql(sql).to_a.length > 0 + end + + describe '#delayed_drop' do + it "can drop a table after correct delay and when new table exists" do + ActiveRecord::Base.exec_sql "CREATE TABLE table_with_old_name (topic_id INTEGER)" + + name = ActiveRecord::Base + .exec_sql("SELECT name FROM schema_migration_details LIMIT 1") + .getvalue(0,0) + + Topic.exec_sql("UPDATE schema_migration_details SET created_at = :created_at WHERE name = :name", + name: name, created_at: 15.minutes.ago) + + dropped_proc_called = false + + described_class.delayed_drop( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: name, + delay: 20.minutes, + on_drop: ->(){dropped_proc_called = true} + ) + + expect(table_exists?('table_with_old_name')).to eq(true) + expect(dropped_proc_called).to eq(false) + + described_class.delayed_drop( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: name, + delay: 10.minutes, + on_drop: ->(){dropped_proc_called = true} + ) + + expect(table_exists?('table_with_old_name')).to eq(true) + expect(dropped_proc_called).to eq(false) + + ActiveRecord::Base.exec_sql "CREATE TABLE table_with_new_name (topic_id INTEGER)" + + described_class.delayed_drop( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: name, + delay: 10.minutes, + on_drop: ->(){dropped_proc_called = true} + ) + + expect(table_exists?('table_with_old_name')).to eq(false) + expect(dropped_proc_called).to eq(true) + end + end +end