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"
################################################################################