require 'db/sync/version' require 'db/sync/diff' require 'rails' require 'active_record' require 'db/sync/model' module Db # Databse Sync class Sync include ActiveSupport::Configurable attr_accessor :sync_dir, :custom_tables def initialize(sync_dir = nil, tables = nil) self.sync_dir = File.join((Rails.root || '.'), 'db', (sync_dir || 'data')) self.custom_tables = tables end def log @log ||= [] end def sync_up(commit = true) @log = [] # TODO: change to row by row loading working_tables.each do |table| table_model = data_model(table) fail 'Tables without id are not supported!' unless table_model.include_id? data = File.read(table_filename(table)) all_records = YAML.load(data) current_records = table_model.records diff = Db::Sync::Diff.new(current_records, all_records, table_model.pkey) delete_records(table, diff.deletes, commit) update_records(table, diff.updates, commit) insert_records(table, diff.inserts, commit) # TODO: fix auto increments end end def insert_records(table, inserts, commit = true) inserts.each do |record| log << "[#{table}] INSERT #{record}" next unless commit insert_manager = Arel::InsertManager.new(ActiveRecord::Base) arel_model = Arel::Table.new(table) insert_data = record.map do |key, value| [arel_model[key], value] end insert_manager.insert(insert_data) # print "#{insert_manager.to_sql}\n" ActiveRecord::Base.connection.execute(insert_manager.to_sql) end end def delete_records(table, deletes, commit = false) arel_model = Arel::Table.new(table) deletes.each do |delete_params| log << "[#{table}] DELETE #{delete_params}" next unless commit delete_manager = Arel::DeleteManager.new(ActiveRecord::Base) delete_manager.from(arel_model) delete_data = delete_params.map do |key, value| [arel_model[key].eq(value)] end delete_manager.where(delete_data) # print "#{delete_manager.to_sql}\n" ActiveRecord::Base.connection.execute(delete_manager.to_sql) end end def update_records(table, updates, commit = false) arel_model = Arel::Table.new(table) updates.each do |update| log << "[#{table}] UPDATE #{update[:key]} with #{update[:changes]}" next unless commit update_manager = Arel::UpdateManager.new(ActiveRecord::Base) update_key = update[:key].map do |key, value| [arel_model[key].eq(value)] end update_changes = update[:changes].map do |key, value| [arel_model[key], value] end update_manager.table(arel_model).where(update_key).set(update_changes) # print "#{update_manager.to_sql}\n" ActiveRecord::Base.connection.execute(update_manager.to_sql) end end def sync_down # TODO: change to row by row saving Dir.mkdir(sync_dir) unless Dir.exist?(sync_dir) working_tables.each do |table| File.open(table_filename(table), 'w') do |f| current_records = table_model_records(table) f << current_records.to_yaml end end end def table_model_records(table) # TODO: Some kind of paging table_model = data_model(table) table_model.records end def working_tables custom_tables || config.tables || all_tables end def all_tables ActiveRecord::Base.connection.tables.reject do |table| %w(schema_info, schema_migrations).include?(table) end end def table_filename(table) File.join(sync_dir, "#{table}.yml") end def configure yield(config) end def data_model(table) result = Class.new(Db::Sync::Model) result.table_name = table result end # Railtie needed, so that rake tasks are appended, when used as a gem class Railtie < Rails::Railtie rake_tasks do load File.expand_path('../../tasks/db.rake', __FILE__) end end end end