William Graham’s blog

October 22, 2008

Some convenience code for using Micah Alles’ Rails migration testing plugin

Filed under: Programming,Ruby on Rails,Web Development — liamgraham @ 9:05 pm

Lately I’ve been using Micah Alles’ migration testing plugin in my Ruby on Rails development. I very much like being able to use it to test migrations, and I’ve also been using it to compare schemas that should be mirrors of each other. The syntax is very nice, with assert_table statements basically mirroring the migration create_table syntax, performing an assertion that the table should be in the target database and should match the structure defined in the statement, such as:

    assert_table :groups do |t|
      t.column :group_id, :integer
      t.column :site_id, :integer, :limit => 10
      t.column :group_desc, :string, :limit => 100
      t.column :fam, :integer, :limit => 10
      t.index ["fam"], :name => :groups_fam_idx
    end

The problem, as you may notice from my syntax above, is that this is set up for the older t.column syntax that was current when Micah wrote the plugin, rather than the newer t.<datatype> syntax. Since I’m using this plugin to check mirrors of databases (often to compare production schemas against dev or test), I wanted a quick way to do something set up assert_table statements using a schema.rb file as a template. I initially looked at the plugin code thinking maybe I would write a version to work with the new syntax, but quickly figured out that I’m just too much of a Ruby newb to tackle that task. I ended up just writing some code which takes a schema.rb file (in the new syntax) as a commandline argument and creates the necessary assert_table statements in the old syntax, which can then be cut and pasted into the migration test file generated from Micah’s template.

Save the code below to a Ruby file (mine is named generate_migration_test_asserts.rb). Usage is simply

  ruby generate_migration_test_asserts.rb schema.rb

That is assuming that you want to read in a schema.rb file and are in the file’s directory. Adjust to suit if you need to read in another file. You can redirect the output to a text file if you’d rather do that than copy from the screen.

I mention in the TODO section of the file header that the code could also be enhanced to do more general translation between the current create_table syntax and the older syntax. Maybe I’ll tackle that when I get a few minutes to spare …

-William

################################################################################
#
# generate_migration_test_asserts.rb
#
# code to translate new-syntax table definitions into the older syntax for use
# in 'assert_table' statements with Micah Alles' migration testing plugin:
#   http://spin.atomicobject.com/2007/02/27/migration-testing-in-rails/
#
# With some modification or enhancement to add a choice between 'assert_table'
# and 'create_table' statements, it could also be used more generally just to
# translate from this:
#
#   create_table "foo", :primary_key => "foo_id", :force => true do |t|
#     t.string   "name", :limit => 40
#     t.string   "desc", :limit => 100
#     t.datetime "created_at"
#     t.string   "created_by",  :limit => 30
#   end
#
# to this:
#
#   create_table "foo", :primary_key => "foo_id", :force => true do |t|
#     t.column :name, :string, :limit => 40
#     t.column :desc, :string, :limit => 100
#     t.column :created_at, :datetime
#     t.column :created_by, :string, :limit => 30
#   end
#
#
# TODO:
#  to enhance for table creation, will need to support the specification of
#  column defaults and nullability
#
# William Graham, 10/22/2008
#
################################################################################

########################################################
#
# helper class for indices
#
########################################################
class Index
  attr_accessor :name, :columns, :unique

  def initialize(name)
    @name = name
  end
end

########################################################
#
# helper class for columns
#
########################################################
class Column
  attr_accessor :name, :type, :limit, :default, :null

  def initialize(name = nil, type = nil, null = nil, limit = nil, default = nil)
    @name, @type, @null, @limit, @default = name, type, null, limit, default
  end
end

########################################################
#
# helper class for tables
#
########################################################

class Table
  attr_accessor :name, :columns, :indices

  def initialize(name = nil, cols = Array.new, inds = Array.new)
    @name, @columns, @indices = name, cols, inds
  end

  # write out an assert_table statement
  def to_test_s
    s = "assert_table :", self.name, ' do |t|', "\n"
    self.columns.each do |c|
      s << "  t.column :" << c.name << ", :" << c.type
      if (c.limit)
        s << ', :limit => ' << c.limit
      end
      s << "\n"
    end

    self.indices.each do |i|
      s << "  t.index [" << i.columns << "], :name => :" << i.name
      if (i.unique)
        s << ', :unique => true'
      end
      s << "\n"
    end

    s << "end"

    return s
  end

  ########################################################
  #
  # parse a create_table line to get the table name plus
  # the name of the primary key column, if it's defined
  #
  ########################################################
  def parse_create(create_str)
    tabname_regex = /^\s*create_table\s+[\:\"\'](\w+)/
    pk_regex = /primary_key\s*\=\>\s*[\:\"\'](\w+)/

    tabname_regex =~ create_str
    data = Regexp.last_match
    self.name = data[1] if (data)

    pk_regex =~ create_str
    data = Regexp.last_match
    self.columns << Column.new(data[1], :integer) if (data)
  end

  ########################################################
  #
  # parse a column definition line to get column name,
  # type, limit, and default value
  #
  ########################################################
  def parse_col(col_str)
    main_regex  = /^\s*\w\.(\w+)\s+[\:\"\'](\w+)/
    limit_regex = /\:limit\s*\=\>\s*(\d+)/
    null_regex  = /\:null\s*\=\>\s*(\w+)/
    default_regex  = /\:default\s*\=\>\s*(\w+)/

    main_regex =~ col_str
    data = Regexp.last_match

    if (data)
      c = Column.new(data[2], data[1])
      limit_regex =~ col_str
      data = Regexp.last_match
      c.limit = data[1] if (data)

      null_regex =~ col_str
      data = Regexp.last_match
      c.null = data[1] if (data)

      default_regex =~ col_str
      data = Regexp.last_match
      c.default = data[1] if (data)

      self.columns << c
    end
  end

  ##########################################################
  #
  # parse an add_index line to get the index name, list of
  # columns, and whether or not it is a unique index
  #
  ##########################################################
  def parse_index(index_str)
    #note: capture the first \w+ if table name is needed in the future
    col_regex = /\[(.*)\]/
    name_regex = /\:name\s*\=\>\s*[\"\'](\w+)/
    unq_regex = /\:unique\s*\=\>\s*(\w+)/

    name_regex =~ index_str
    data = Regexp.last_match

    if (data)
      indx = Index.new(data[1])
      col_regex =~ index_str
      data = Regexp.last_match
      indx.columns = data[1] if (data)

      unq_regex =~ index_str
      data = Regexp.last_match
      indx.unique = data[1] if (data)

      self.indices << indx
    end
  end

  def print_match_data(data)
    if (!data)
      return
    end

    print "DataMatch object:\n"
    0.upto(data.size) do |i|
      print "\t",i,": ",data[i],"\n"
    end
  end
end

################################################################################

fname = ARGV[0]
f = File.open(fname, 'r')

t = nil
f.each do |line|
  if (line =~ /create_table/)
    print t.to_test_s,"\n\n" if (t)
    t = Table.new
    t.parse_create(line)
    next
  end

  if (line =~ /t\./)
    t.parse_col(line)
    next
  end

  if (line =~ /add_index/)
    t.parse_index(line)
    next
  end
end

print t.to_test_s, "\n"

################################################################################
Advertisements

Blog at WordPress.com.