Making migrations easier 0

Posted by carl

One of my most common problems when developing migrations for a rails project happens when I make a mistake in my migration code that causes the migration to fail to complete successfully. When this happens, it is often the case that some of the commands in your migration executed successfully. This means that when you fix your bug and try to run the migration again, it will fail because it will attempt to run the previously successful commands but will complain that the changes they refer to are already there. There are some workarounds for this, such as using the :force => true option, but even then there are some situations that are hard to recover from. I often ended up having to manually drop my database and re-create it and then run the migration again—a real pain. So I finally decided to make a rake task that would make this a lot easier.

namespace :db do
  task :nuke => :environment do
    abcs = ActiveRecord::Base.configurations
    ["development", "test"].each do |db|
      case abcs[db]["adapter"]
        when "oci"
          ActiveRecord::Base.establish_connection(db.to_sym)
          conn = ActiveRecord::Base.connection
          conn.begin_db_transaction
          conn.tables.each do |table|
            indexes = conn.indexes(table)
            indexes.each do |ind|
              puts "Dropping index #{ind.name}"
              conn.execute("DROP INDEX #{ind.name}")
            end
            puts "Dropping table #{table}"
            conn.execute("DROP TABLE #{table}")
          end
          sql = "SELECT LOWER(sequence_name) FROM user_sequences"
          sequences = conn.select_all(sql).inject([]) do |seqs, s|
            seqs << s.to_a.first.last
          end
          sequences.each do |seq|
            puts "Dropping sequence #{seq}"
            conn.execute("DROP SEQUENCE #{seq}")
          end
          conn.commit_db_transaction
        when "mysql"
          ActiveRecord::Base.establish_connection(db.to_sym)
          conn = ActiveRecord::Base.connection
          conn.execute("DROP DATABASE #{abcs[db]["database"]}")
          conn.execute("CREATE DATABASE #{abcs[db]["database"]}")
          ActiveRecord::Base.establish_connection(db.to_sym)
        when "sqlite", "sqlite3"
          dbfile = abcs[db]["database"] || abcs[db]["dbfile"]
          File.delete(dbfile) if File.exist?(dbfile)
          ActiveRecord::Base.establish_connection(db.to_sym)
        else
          raise "Task not supported by '#{abcs[db]["adapter"]}'"
      end
      ENV['RAILS_ENV'] = db
      Rake::Task["db:migrate"].dup.invoke
      Rake::Task["db:fixtures:load"].dup.invoke
    end
  end
end

Save the text above into lib/tasks/db_nuke.rake. This task (run it using rake db:nuke) will completely drop your dev and test databases, run all migrations on them, and run rake db:fixtures:load on them. It works on mysql and sqlite. Adapting it for postgres or some other database is left as an exercise for the reader.

Update: I added a dependency on the environment, rather than trying to include the environment files manually. This is the "right" way to do it.

Update: I have added Oracle support.