Poor man's migrations
Published about 6 years ago

In case you have read PJ’s post on Automatic migrations you might like this.

PoorMansMigrations is a very simple Active Record extension that allows you to create/update/delete DB columns without using migrations directly.

Playing with your models can be as easy as :

require 'rubygems'
require 'activerecord'
require 'poor_mans_migrations'

ActiveRecord::Base.establish_connection :adapter  => "mysql", :host => "localhost",  :username => "root", :database => "property_db"
ActiveRecord::Base.logger = Logger.new($stdout)

class User < ActiveRecord::Base
  column :age, :string
  column :name, :string
  column :admin, :integer, :default => 0
end

User.migrate

u = User.create :age => '1000', :name => 'whatever'

Doing Model.migrate will automatically sync the database table with the columns you define inside your models. That is, if you add new column :what, :ever statements, those columns will be created in the table. Similarly, if you remove any column statements, respective columns will be removed from the table.

But hey, thats just stupid. Age is integer silly! Changing the column is a little tricky. As I didn’t want the library to be super smart in figuring out what changed when, I just used a simple/stupid/verbose solution. If you supply :force => true option to column definition, the column will be dropped and recreated when you do Model.migrate

So the following will fix the age column :

class User < ActiveRecord::Base
  column :age, :integer, :force => true
end

User.migrate

And if you royally screw up, and just want to start everything from scratch :

User.migrate!

Migrate with a !

Here’s the code for PoorMansMigrations :

# Released under WTFPL - http://sam.zoy.org/wtfpl/
module PoorMansMigrations
  def self.extended(base)
    base.class_inheritable_accessor :migration_columns
    base.migration_columns = []
  end

  def column(name, type, options = {})
    self.migration_columns << {:column_name => name, :column_type => type, :options => options}
  end

  def realize!(force_drop = false)
    # Force drop if needed
    connection.drop_table(table_name) if force_drop && table_exists?

    # Create table
    connection.create_table(table_name) {|t| } unless table_exists?

    self.migration_columns.each do |p|
      #haxhaxhax - Force reload reading column names. Whatever.
      reset_column_information
      column_exists = column_names.include?(p[:column_name].to_s)

      # Delete the columns if forced to
      if p[:options].delete(:force) && column_exists
        connection.remove_columns(table_name, p[:column_name])
        column_exists = false
      end

      connection.add_column(table_name, p[:column_name], p[:column_type], p[:options]) unless column_exists
    end
  end

  def clean_leftover_columns!
    return unless table_exists?
    reset_column_information

    left_overs = column_names - self.migration_columns.map {|m| m[:column_name].to_s} - Array(primary_key)
    connection.remove_columns(table_name, left_overs)
    reset_column_information
  end

  # Force recreation for everything
  def migrate!
    realize!(true)
    clean_leftover_columns!
  end

  # Take it easy. Only force if specified in column definition
  def migrate
    realize!
    clean_leftover_columns!
  end
end

ActiveRecord::Base.send :extend, PoorMansMigrations

You get the Gist

Main purpose of PoorMansMigrations is to make it very simple to play around with Active Record, independent of Rails. In stand alone scripts, etc. It should be very simple for anyone to write the needed plugin in order to use it in regular Rails apps. PDI.

Please note that you would never want to use this code/method in any production application. Destructive migrations can cost you your job.

UPDATE 1 : I had made a schoolboy error in the initial version of the code, by not using class inheritable attributes. It’s been fixed now.