diff --git a/lib/tasks/migrate_milestones_and_statuses.rake b/lib/tasks/migrate_milestones_and_statuses.rake new file mode 100644 index 000000000..a16b9479e --- /dev/null +++ b/lib/tasks/migrate_milestones_and_statuses.rake @@ -0,0 +1,92 @@ +namespace :milestones do + + def generate_table_migration_sql(new_table:, old_table:, columns:) + from_cols = ['id', *columns.keys] + to_cols = ['id', *columns.values] + <<~SQL + INSERT INTO #{new_table} (#{to_cols.join(', ')}) + SELECT #{from_cols.join(', ')} FROM #{old_table}; + SQL + end + + def migrate_table!(new_table:, old_table:, columns:) + puts "Migrating data from '#{old_table}' to '#{new_table}'..." + result = ActiveRecord::Base.connection.execute( + generate_table_migration_sql(old_table: old_table, + new_table: new_table, + columns: columns) + + ) + puts "#{result.cmd_tuples} rows affected" + end + + def populate_column!(table:, column:, value:) + puts "Populating column '#{column}' from table '#{table}' with '#{value}'..." + result = ActiveRecord::Base.connection.execute( + "UPDATE #{table} SET #{column} = '#{value}';" + ) + puts "#{result.cmd_tuples} rows affected" + end + + def count_rows(table) + ActiveRecord::Base.connection.query("SELECT COUNT(*) FROM #{table};")[0][0].to_i + end + + desc "Migrate milestones and milestone status data after making the model polymorphic" + task migrate: :environment do + # This script copies all milestone-related data from the old tables to + # the new ones (preserving all primary keys). All 3 of the new tables + # must be empty. + # + # To clear the new tables to test this script: + # + # DELETE FROM milestone_statuses; + # DELETE FROM milestones; + # DELETE FROM milestone_translations; + # + + start = Time.now + + ActiveRecord::Base.transaction do + migrate_table! old_table: 'budget_investment_statuses', + new_table: 'milestone_statuses', + columns: {'name' => 'name', + 'description' => 'description', + 'hidden_at' => 'hidden_at', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at'} + + migrate_table! old_table: 'budget_investment_milestones', + new_table: 'milestones', + columns: {'investment_id' => 'milestoneable_id', + 'title' => 'title', + 'description' => 'description', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at', + 'publication_date' => 'publication_date', + 'status_id' => 'status_id'} + + populate_column! table: 'milestones', + column: 'milestoneable_type', + value: 'Budget::Investment' + + migrate_table! old_table: 'budget_investment_milestone_translations', + new_table: 'milestone_translations', + columns: {'budget_investment_milestone_id' => 'milestone_id', + 'locale' => 'locale', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at', + 'title' => 'title', + 'description' => 'description'} + + puts "Verifying that all rows were copied..." + unless count_rows('milestones') == count_rows('budget_investment_milestones') && + count_rows('milestone_statuses') == count_rows('budget_investment_statuses') && + count_rows('milestone_translations') == count_rows('budget_investment_milestone_translations') + raise "Number of rows of old and new tables do not match! Rolling back transaction..." + end + end + + puts "Finished in %.3f seconds" % (Time.now - start) + end +end diff --git a/spec/lib/tasks/milestones_spec.rb b/spec/lib/tasks/milestones_spec.rb new file mode 100644 index 000000000..5feb6206b --- /dev/null +++ b/spec/lib/tasks/milestones_spec.rb @@ -0,0 +1,121 @@ +require "rails_helper" +require "rake" + +describe "Milestones tasks" do + before do + Rake.application.rake_require "tasks/migrate_milestones_and_statuses" + Rake::Task.define_task(:environment) + end + + describe "#migrate" do + let :run_rake_task do + Rake::Task["milestones:migrate"].reenable + Rake.application.invoke_task "milestones:migrate" + end + + let!(:investment) { create(:budget_investment) } + + before do + ActiveRecord::Base.connection.execute( + "INSERT INTO budget_investment_statuses " + + "(name, description, hidden_at, created_at, updated_at) " + + "VALUES ('open', 'Good', NULL, '#{Time.current - 1.day}', '#{Time.current}');" + ) + + status_id = ActiveRecord::Base.connection.execute( + "SELECT MAX(id) FROM budget_investment_statuses;" + ).to_a.first["max"] + + milestone_attributes = { + investment_id: investment.id, + title: "First", + description: "Interesting", + publication_date: Date.yesterday, + status_id: status_id, + created_at: Time.current - 1.day, + updated_at: Time.current + } + + ActiveRecord::Base.connection.execute( + "INSERT INTO budget_investment_milestones " + + "(#{milestone_attributes.keys.join(", ")}) " + + "VALUES (#{milestone_attributes.values.map { |value| "'#{value}'"}.join(", ")})" + ) + end + + it "migrates statuses" do + run_rake_task + + expect(Milestone::Status.count).to be 1 + + status = Milestone::Status.first + expect(status.name).to eq "open" + expect(status.description).to eq "Good" + expect(status.hidden_at).to be nil + expect(status.created_at.to_date).to eq Date.yesterday + expect(status.updated_at.to_date).to eq Date.today + end + + it "migrates milestones" do + run_rake_task + + expect(Milestone.count).to be 1 + + milestone = Milestone.first + expect(milestone.milestoneable_id).to eq investment.id + expect(milestone.milestoneable_type).to eq "Budget::Investment" + expect(milestone.title).to eq "First" + expect(milestone.description).to eq "Interesting" + expect(milestone.publication_date).to eq Date.yesterday + expect(milestone.status_id).to eq Milestone::Status.first.id + expect(milestone.created_at.to_date).to eq Date.yesterday + expect(milestone.updated_at.to_date).to eq Date.today + end + + context "Statuses had been deleted" do + before do + ActiveRecord::Base.connection.execute( + "INSERT INTO budget_investment_statuses " + + "(name, description, hidden_at, created_at, updated_at) " + + "VALUES ('deleted', 'Del', NULL, '#{Time.current - 1.day}', '#{Time.current}');" + ) + + ActiveRecord::Base.connection.execute( + "DELETE FROM budget_investment_statuses WHERE name='deleted'" + ) + + ActiveRecord::Base.connection.execute( + "INSERT INTO budget_investment_statuses " + + "(name, description, hidden_at, created_at, updated_at) " + + "VALUES ('new', 'New', NULL, '#{Time.current - 1.day}', '#{Time.current}');" + ) + + status_id = ActiveRecord::Base.connection.execute( + "SELECT MAX(id) FROM budget_investment_statuses;" + ).to_a.first["max"] + + milestone_attributes = { + investment_id: investment.id, + title: "Last", + description: "Different", + publication_date: Date.yesterday, + status_id: status_id, + created_at: Time.current - 1.day, + updated_at: Time.current + } + + ActiveRecord::Base.connection.execute( + "INSERT INTO budget_investment_milestones " + + "(#{milestone_attributes.keys.join(", ")}) " + + "VALUES (#{milestone_attributes.values.map { |value| "'#{value}'"}.join(", ")})" + ) + end + + it "migrates the status id correctly" do + run_rake_task + + expect(Milestone.last.status_id).to eq Milestone::Status.last.id + end + end + end +end