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