diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..860487c --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.7.1 diff --git a/.travis.yml b/.travis.yml index 772ca77..eebff45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ sudo: false language: ruby rvm: - - 2.2.3 -before_install: gem install bundler -v 1.12.2 + - 2.4.4 + - 2.5.5 + - 2.6.2 + - 2.7.1 +script: bundle exec rspec diff --git a/dump-hook.gemspec b/dump-hook.gemspec index 8f4cff4..636cb5a 100644 --- a/dump-hook.gemspec +++ b/dump-hook.gemspec @@ -29,9 +29,9 @@ Gem::Specification.new do |spec| spec.add_dependency 'timecop' - spec.add_development_dependency "bundler", "~> 1.12" - spec.add_development_dependency "rake", "~> 10.0" - spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "bundler", ">= 2.2.10" + spec.add_development_dependency "rake", ">= 12.3.3" + spec.add_development_dependency "rspec" spec.add_development_dependency "sequel" spec.add_development_dependency "pg" spec.add_development_dependency "mysql2" diff --git a/lib/dump_hook.rb b/lib/dump_hook.rb index fd109c1..506eb98 100644 --- a/lib/dump_hook.rb +++ b/lib/dump_hook.rb @@ -1,5 +1,8 @@ require "dump_hook/version" +require "dump_hook/hooks/mysql" +require "dump_hook/hooks/postgres" require "timecop" +require "ostruct" module DumpHook class Settings @@ -11,23 +14,39 @@ class Settings :username, :password, :host, - :port + :port, + :sources, + :with_sources def initialize - @database = 'please set database' @database_type = 'postgres' @dumps_location = 'tmp/dump_hook' @remove_old_dumps = true + @with_sources = false + @sources = {} end end class << self attr_accessor :settings - end + attr_accessor :hooks - def self.setup - self.settings = Settings.new - yield(settings) + def setup + self.settings = Settings.new + yield(settings) + unless settings.sources.empty? + settings.with_sources = true + end + unless settings.database.nil? + single_source = { type: settings.database_type.to_sym, + database: settings.database, + username: settings.username, + password: settings.password, + host: settings.host, + port: settings.port } + self.settings.sources[settings.database_type.to_sym] = single_source + end + end end def execute_with_dump(name, opts={}, &block) @@ -35,7 +54,7 @@ def execute_with_dump(name, opts={}, &block) actual = opts[:actual] || settings.actual create_dirs_if_not_exists filename = full_filename(name, created_on, actual) - if File.exists?(filename) + if File.exist?(filename) restore_dump(filename) else if created_on @@ -54,30 +73,36 @@ def settings end def store_dump(filename) - case settings.database_type - when 'postgres' - args = ['-a', '-x', '-O', '-f', filename, '-Fc', '-T', 'schema_migrations'] - args.concat(pg_connection_args) - Kernel.system("pg_dump", *args) - when 'mysql' - args = mysql_connection_args - args << "--compress" - args.concat ["--result-file", filename] - args.concat ["--ignore-table", "#{settings.database}.schema_migrations"] - Kernel.system("mysqldump", *args) + FileUtils.mkdir_p(filename) + settings.sources.each do |name, parameters| + filename_with_namespace = File.join(filename, "#{name}.dump") + connection_settings = OpenStruct.new(parameters.slice(:database, :username, :password, :port, :host)) + dumper = case parameters[:type] + when :postgres + Hooks::Postgres.new(connection_settings) + when :mysql + Hooks::MySql.new(connection_settings) + else + raise "Unsupported type of source" + end + dumper.dump(filename_with_namespace) end end def restore_dump(filename) - case settings.database_type - when 'postgres' - args = pg_connection_args - args << filename - Kernel.system("pg_restore", *args) - when 'mysql' - args = mysql_connection_args - args.concat ["-e", "source #{filename}"] - Kernel.system("mysql", *args) + FileUtils.mkdir_p(filename) + settings.sources.each do |name, parameters| + filename_with_namespace = File.join(filename, "#{name}.dump") + connection_settings = OpenStruct.new(parameters.slice(:database, :username, :password, :port, :host)) + dumper = case parameters[:type] + when :postgres + Hooks::Postgres.new(connection_settings) + when :mysql + Hooks::MySql.new(connection_settings) + else + raise "Unsupported type of source" + end + dumper.restore(filename_with_namespace) end end @@ -88,25 +113,12 @@ def full_filename(name, created_on, actual) elsif actual name_with_created_on = "#{name_with_created_on}_actual#{actual}" end - "#{settings.dumps_location}/#{name_with_created_on}.dump" + full_path = "#{settings.dumps_location}/#{name_with_created_on}" + + settings.with_sources ? full_path : "#{full_path}.dump" end def create_dirs_if_not_exists FileUtils.mkdir_p(settings.dumps_location) end - - def mysql_connection_args - args = [settings.database] - args.concat ['--user', settings.username] if settings.username - args << "--password=#{settings.password}" if settings.password - args - end - - def pg_connection_args - args = ['-d', settings.database] - args.concat(['-U', settings.username]) if settings.username - args.concat(['-h', settings.host]) if settings.host - args.concat(['-p', settings.port]) if settings.port - args - end end diff --git a/lib/dump_hook/hooks/connectable.rb b/lib/dump_hook/hooks/connectable.rb new file mode 100644 index 0000000..329f1dc --- /dev/null +++ b/lib/dump_hook/hooks/connectable.rb @@ -0,0 +1,9 @@ +module DumpHook + module Hooks + module Connectable + def initialize(connection_settings) + @connection_settings = connection_settings + end + end + end +end diff --git a/lib/dump_hook/hooks/mysql.rb b/lib/dump_hook/hooks/mysql.rb new file mode 100644 index 0000000..dacc7d4 --- /dev/null +++ b/lib/dump_hook/hooks/mysql.rb @@ -0,0 +1,27 @@ +require "dump_hook/hooks/connectable" + +module DumpHook + module Hooks + class MySql + include ::DumpHook::Hooks::Connectable + + def dump(filename) + args = [@connection_settings.database] + args << "--compress" + args.concat(["--result-file", filename]) + args.concat(["--ignore-table", "#{@connection_settings.database}.schema_migrations"]) + args.concat(['--user', @connection_settings.username]) if @connection_settings.username + args << "--password=#{@connection_settings.password}" if @connection_settings.password + Kernel.system("mysqldump", *args) + end + + def restore(filename) + args = [@connection_settings.database] + args.concat(["-e", "source #{filename}"]) + args.concat(['--user', @connection_settings.username]) if @connection_settings.username + args << "--password=#{@connection_settings.password}" if @connection_settings.password + Kernel.system("mysql", *args) + end + end + end +end diff --git a/lib/dump_hook/hooks/postgres.rb b/lib/dump_hook/hooks/postgres.rb new file mode 100644 index 0000000..1768f7d --- /dev/null +++ b/lib/dump_hook/hooks/postgres.rb @@ -0,0 +1,27 @@ +require "dump_hook/hooks/connectable" + +module DumpHook + module Hooks + class Postgres + include ::DumpHook::Hooks::Connectable + + def dump(filename) + args = ['-a', '-x', '-O', '-f', filename, '-Fc', '-T', 'schema_migrations'] + args.concat(['-d', @connection_settings.database]) + args.concat(['-U', @connection_settings.username]) if @connection_settings.username + args.concat(['-h', @connection_settings.host]) if @connection_settings.host + args.concat(['-p', @connection_settings.port.to_s]) if @connection_settings.port + Kernel.system("pg_dump", *args) + end + + def restore(filename) + args = ['-d', @connection_settings.database] + args.concat(['-U', @connection_settings.username]) if @connection_settings.username + args.concat(['-h', @connection_settings.host]) if @connection_settings.host + args.concat(['-p', @connection_settings.port.to_s]) if @connection_settings.port + args << filename + Kernel.system("pg_restore", *args) + end + end + end +end diff --git a/lib/dump_hook/version.rb b/lib/dump_hook/version.rb index 0e0c7d0..5e3f2a6 100644 --- a/lib/dump_hook/version.rb +++ b/lib/dump_hook/version.rb @@ -1,3 +1,3 @@ module DumpHook - VERSION = "0.0.1" + VERSION = "1.0.0.rc" end diff --git a/spec/dump_hook_spec.rb b/spec/dump_hook_spec.rb index 73731de..51251fb 100644 --- a/spec/dump_hook_spec.rb +++ b/spec/dump_hook_spec.rb @@ -21,10 +21,6 @@ expect(DumpHook.settings.remove_old_dumps).to eq(true) end - it 'sets database' do - expect(DumpHook.settings.database).to eq('please set database') - end - it 'does not set actual' do expect(DumpHook.settings.actual).to be nil end @@ -77,7 +73,7 @@ end describe '.execute_with_dump' do - let(:object) { object = Object.new } + let(:object) { Object.new } before(:each) do object.extend(DumpHook) @@ -98,72 +94,95 @@ it 'creates folders' do object.execute_with_dump("some_dump") { } - expect(Dir.exists?(dumps_location)).to be(true) + expect(Dir.exist?(dumps_location)).to be(true) end end - context 'postgres' do - let(:database) { 'dump_hook_test' } - let(:db) { Sequel.connect(adapter: 'postgres', database: database) } + shared_context "mysql db init" do + let(:mysql_db_name) { 'dump_hook_test' } + let(:mysql_username) { 'root' } + let(:mysql_db) { Sequel.connect(adapter: 'mysql2', user: mysql_username) } + + before(:each) do + mysql_db.run("CREATE DATABASE #{mysql_db_name}") + mysql_db.run("USE #{mysql_db_name}") + end + + after(:each) do + mysql_db.run("DROP DATABASE #{mysql_db_name}") + + mysql_db.disconnect + end + end + + shared_context "postgres db init" do + let(:postgres_db_name) { 'dump_hook_test' } + let(:postgres_db) { Sequel.connect(adapter: 'postgres', database: postgres_db_name) } + + before(:each) do + Kernel.system('createdb', postgres_db_name) + end + after(:each) do + postgres_db.disconnect + Kernel.system("dropdb", postgres_db_name) + end + end + + shared_examples_for 'data insertion and restoring' do before(:each) do - Kernel.system('createdb', database) + object.execute_with_dump("some_dump") do + db.run("create table t (a text, b text)") + db.run("insert into t values ('a', 'b')") + end + end + + it 'inserts some info' do + expect(db[:t].map([:a, :b])).to eq([['a', 'b']]) + end + it 'uses dump content if dump exists' do + db.run("delete from t") + expect { object.execute_with_dump("some_dump") { } }.to change { db[:t].map([:a, :b]) }.to([['a', 'b']]) + end + end + + context 'postgres' do + include_context "postgres db init" + let(:db) { postgres_db } + + before(:each) do DumpHook.setup do |c| - c.database = database + c.database = postgres_db_name end end after(:each) do - db.disconnect - Kernel.system('dropdb', database) FileUtils.rm_r('tmp') end it 'creates dump file' do object.execute_with_dump("some_dump") { } - expect(File.exists?("tmp/dump_hook/some_dump.dump")).to be(true) + expect(File.exist?("tmp/dump_hook/some_dump.dump")).to be(true) end - context 'dump content' do - before(:each) do - object.execute_with_dump("some_dump") do - db.run("create table t (a text, b text)") - db.run("insert into t values ('a', 'b')") - end - end - - it 'inserts some info' do - expect(db[:t].map([:a, :b])).to eq([['a', 'b']]) - end - - it 'uses dump content if dump exists' do - db.run("delete from t") - object.execute_with_dump("some_dump") { } - expect(db[:t].map([:a, :b])).to eq([['a', 'b']]) - end + it_behaves_like "data insertion and restoring" do + let(:db) { postgres_db } end end context 'mysql' do - let(:database) { 'dump_hook_test' } - let(:username) { 'root' } - let(:db) { Sequel.connect(adapter: 'mysql2', user: username) } + include_context "mysql db init" before(:each) do - db.run("CREATE DATABASE #{database}") - db.run("USE #{database}") DumpHook.setup do |c| - c.database = database + c.database = mysql_db_name c.database_type = 'mysql' - c.username = username + c.username = mysql_username end end after(:each) do - db.run("DROP DATABASE #{database}") - - db.disconnect FileUtils.rm_r('tmp') end @@ -172,24 +191,41 @@ expect(File.exists?("tmp/dump_hook/some_dump.dump")).to be(true) end - context 'dump content' do - before(:each) do - object.execute_with_dump("some_dump") do - db.run("create table t (a text, b text)") - db.run("insert into t values ('a', 'b')") - end - end + it_behaves_like "data insertion and restoring" do + let(:db) { mysql_db } + end + end - it 'inserts some info' do - expect(db[:t].map([:a, :b])).to eq([['a', 'b']]) - end + context "multi DBs" do + include_context "postgres db init" + include_context "mysql db init" - it 'uses dump content if dump exists' do - db.run("delete from t") - object.execute_with_dump("some_dump") { } - expect(db[:t].map([:a, :b])).to eq([['a', 'b']]) + before(:each) do + FileUtils.mkdir_p("tmp") + + DumpHook.setup do |c| + c.sources = { primary: { type: :postgres, database: postgres_db_name }, + secondary: { type: :mysql, username: mysql_username, database: mysql_db_name } } end end + + after(:each) do + FileUtils.rm_r('tmp') + end + + it "creates dump files" do + object.execute_with_dump("some_dump") { } + expect(File.exist?("tmp/dump_hook/some_dump/primary.dump")).to be(true) + expect(File.exist?("tmp/dump_hook/some_dump/secondary.dump")).to be(true) + end + + it_behaves_like "data insertion and restoring" do + let(:db) { mysql_db } + end + + it_behaves_like "data insertion and restoring" do + let(:db) { postgres_db } + end end end end