Skip to content

Instantly share code, notes, and snippets.

@andynu
Created September 24, 2011 21:56
Show Gist options
  • Save andynu/1239916 to your computer and use it in GitHub Desktop.
Save andynu/1239916 to your computer and use it in GitHub Desktop.
Reverse engineer ActiveRecord definitions by examining an existing (mysql) database.
#!/usr/bin/env ruby
# encoding: utf-8
#
# Reverse engineer ActiveRecord definitions
# by examining an existing database.
#
# This is mysql spesific at the moment.
#
# % active_record_by_inference -h
# Usage: active_record_by_inference.rb [options] [table] [table] ...
# -c, --config FILE A rails database.yml [defautl=~/.database.yml]
# -o, --output DIRECTORY The directory to write model files into
# (otherwise classes are printed to stdout)
# -l, --database KEY A key from your database.yml
# -h, --help Display this screen
#
#
require 'logger'
require 'active_record'
class ActiveRecordByInference
class Base < ActiveRecord::Base
def self.connect_to(connection_hashes, key)
self.establish_connection(connection_hashes[key])
end
end
end
class ActiveRecordByInference
@@log = Logger.new(STDERR)
def initialize(config={})
@config = config
database_config_file = config[:config_file]
connection_key = config[:database_key]
connection_hashes = YAML.load_file(database_config_file)
@conn = ActiveRecordByInference::Base.connect_to(connection_hashes,connection_key).connection
end
def run
table_descriptions = {}
tables = if @config[:tables] && !@config[:tables].empty?
list_tables() & @config[:tables]
else
list_tables()
end
tables.each do |table|
table_descriptions[table] = desc_table(table)
end
puts generate_classes(table_descriptions)
end
def list_tables()
list = []
@conn.execute("show tables").each do |row|
list << row[0]
end
list
end
def desc_table(table)
desc = []
@conn.execute("describe #{table}").each do |row|
desc << {
:column => row[0],
:type => row[1],
:null => row[2] == "YES",
:primary => row[3] == "PRI",
:default => row[4],
:auto_increment => row[5] == "auto_increment",
}
end
desc
end
def generate_associations(table_descriptions)
associations = {}
has_many = {}
table_names = table_descriptions.keys.inject({}) do |hash,table|
hash[table.pluralize] = table
hash[table.singularize] = table
hash
end
ref_keys = []
table_descriptions.each_pair do |table,cols|
ref_keys << "#{table.singularize.underscore}_id"
end
table_descriptions.each_pair do |table,cols|
cols.each do |col|
column = col[:column]
ref_table = column.gsub(/_id$/,'').singularize
if ref_keys.include? column
associations[table] ||= []
associations[table] << " belongs_to :#{ref_table}"
associations[table_names[ref_table]] ||= []
associations[table_names[ref_table]] << " has_many :#{table.pluralize}"
end
end
end
associations
end
def write_file(table,class_src)
filename = File.join(@config[:output_dir],"#{table.singularize.underscore}.rb")
File.open(filename,'w') do |file|
@@log.info " wrote #{filename}"
file.puts "# #{filename}"
file.puts class_src
end
end
def generate_classes(table_descriptions)
associations = generate_associations(table_descriptions)
src = ""
table_descriptions.each_pair do |table, desc|
class_src = generate_class(table,desc, associations[table] || [])
write_file(table,class_src) if @config[:output_dir]
src << class_src
end
src
end
def generate_class(table, desc, associations=[])
class_name = table.camelize.singularize
clazz = []
clazz << "# #{table}"
clazz << "class #{class_name} < ActiveRecord::Base"
clazz << set_table_name(table,desc)
clazz << set_primary_key(table,desc)
clazz << validations(table,desc)
clazz.concat associations
clazz << "end\n\n"
clazz.compact.join("\n")
end
def set_table_name(table,desc)
unless table == table.pluralize
"set_table_name '#{table}'"
end
end
def set_primary_key(table,desc)
pk = desc.select{|col| col[:primary]}.map{|col| ":#{col[:column]}"}
if pk.size == 1
" set_primary_key #{pk.first}" unless pk.first == ':id'
elsif pk.size > 1
" # require 'composite_primary_keys'\n set_primary_keys #{pk.join(', ')}"
else
nil
end
end
def validations(table,desc)
not_null = desc.select{|col| col[:column] != 'id' && !col[:null] }.map{|col| col[:column]}.map{|col| ":#{col}"}
unless not_null.empty?
" validates_precense_of #{not_null.join(', ')}"
else
nil
end
end
end
if $0 == __FILE__ then
require 'optparse'
opts = {
:config_file => File.join(ENV["HOME"],".database.yml"),
:database_key => 'default',
:tables => ARGV,
}
OptionParser.new do |opt|
opt.banner = "Usage: active_record_by_inference.rb [options] [table] [table] ..."
opt.on( '-c', '--config FILE', 'A rails database.yml [defautl=~/.database.yml]' ) do |config|
opts[:config_file] = config
end
opt.on( '-o', '--output DIRECTORY', "The directory to write model files into \n\t\t\t\t (otherwise classes are printed to stdout)" ) do |output_dir|
opts[:output_dir] = output_dir
end
opt.on( '-l', '--database KEY', 'A key from your database.yml' ) do |database|
opts[:database_key] = database
end
opt.on( '-h', '--help', 'Display this screen' ) do
puts opt
exit
end
end.parse!
ActiveRecordByInference.new(opts).run()
end
@andynu
Copy link
Author

andynu commented Sep 24, 2011

I encounter lots of legacy databases. This
is a pretty basic script for bootstrapping
an old database into active record.

It is only observing

  • set_table_name
  • set_primary_key
  • set_primary_keys
  • validates_presence_of
  • has_many
  • belongs_to

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment