require 'rubygems' require 'yaml' require 'active_record' module YamlDb def self.dump(filename) disable_logger YamlDb::Dump.dump(File.new(filename, "w")) reenable_logger end def self.load(filename) disable_logger YamlDb::Load.load(File.new(filename, "r")) reenable_logger end def self.disable_logger @@old_logger = ActiveRecord::Base.logger ActiveRecord::Base.logger = nil end def self.reenable_logger ActiveRecord::Base.logger = @@old_logger end end module YamlDb::Utils def self.chunk_records(records) yaml = [ records ].to_yaml yaml.sub!("--- \n", "") yaml.sub!('- - -', ' - -') yaml end def self.unhash(hash, keys) keys.map { |key| hash[key] } end def self.unhash_records(records, keys) records.each_with_index do |record, index| records[index] = unhash(record, keys) end records end def self.convert_booleans(records, columns) records.each do |record| columns.each do |column| next if is_boolean(record[column]) record[column] = (record[column] == 't' or record[column] == '1') end end records end def self.boolean_columns(table) columns = ActiveRecord::Base.connection.columns(table).reject { |c| c.type != :boolean } columns.map { |c| c.name } end def self.is_boolean(value) value.kind_of?(TrueClass) or value.kind_of?(FalseClass) end def self.quote_table(table) ActiveRecord::Base.connection.quote_table_name(table) end end module YamlDb::Dump def self.dump(io) tables.each do |table| dump_table(io, table) end end def self.tables ActiveRecord::Base.connection.tables.reject { |table| ['schema_info', 'schema_migrations'].include?(table) } end def self.dump_table(io, table) return if table_record_count(table).zero? dump_table_columns(io, table) dump_table_records(io, table) end def self.dump_table_columns(io, table) io.write("\n") io.write({ table => { 'columns' => table_column_names(table) } }.to_yaml) end def self.dump_table_records(io, table) table_record_header(io) column_names = table_column_names(table) each_table_page(table) do |records| rows = YamlDb::Utils.unhash_records(records, column_names) io.write(YamlDb::Utils.chunk_records(records)) end end def self.table_record_header(io) io.write(" records: \n") end def self.table_column_names(table) ActiveRecord::Base.connection.columns(table).map { |c| c.name } end def self.each_table_page(table, records_per_page=1000) total_count = table_record_count(table) pages = (total_count.to_f / records_per_page).ceil - 1 id = table_column_names(table).first boolean_columns = YamlDb::Utils.boolean_columns(table) quoted_table_name = YamlDb::Utils.quote_table(table) (0..pages).to_a.each do |page| sql = ActiveRecord::Base.connection.add_limit_offset!("SELECT * FROM #{quoted_table_name} ORDER BY #{id}", :limit => records_per_page, :offset => records_per_page * page ) records = ActiveRecord::Base.connection.select_all(sql) records = YamlDb::Utils.convert_booleans(records, boolean_columns) yield records end end def self.table_record_count(table) ActiveRecord::Base.connection.select_one("SELECT COUNT(*) FROM #{YamlDb::Utils.quote_table(table)}").values.first.to_i end end module YamlDb::Load def self.load(io) ActiveRecord::Base.connection.transaction do YAML.load_documents(io) do |ydoc| ydoc.keys.each do |table_name| next if ydoc[table_name].nil? load_table(table_name, ydoc[table_name]) end end end end def self.truncate_table(table) begin ActiveRecord::Base.connection.execute("TRUNCATE #{YamlDb::Utils.quote_table(table)}") rescue Exception ActiveRecord::Base.connection.execute("DELETE FROM #{YamlDb::Utils.quote_table(table)}") end end def self.load_table(table, data) column_names = data['columns'] truncate_table(table) load_records(table, column_names, data['records']) reset_pk_sequence!(table) end def self.load_records(table, column_names, records) quoted_column_names = column_names.map { |column| ActiveRecord::Base.connection.quote_column_name(column) }.join(',') quoted_table_name = YamlDb::Utils.quote_table(table) records.each do |record| ActiveRecord::Base.connection.execute("INSERT INTO #{quoted_table_name} (#{quoted_column_names}) VALUES (#{record.map { |r| ActiveRecord::Base.connection.quote(r) }.join(',')})") end end def self.reset_pk_sequence!(table_name) if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) ActiveRecord::Base.connection.reset_pk_sequence!(table_name) end end end