├── spec ├── lib │ ├── fixtures │ │ ├── rb │ │ │ ├── up.rb │ │ │ └── down.rb │ │ ├── sql │ │ │ ├── down.sql │ │ │ └── up.sql │ │ ├── py │ │ │ ├── down.py │ │ │ └── up.py │ │ ├── js │ │ │ ├── up.js │ │ │ └── down.js │ │ └── go │ │ │ ├── down.go │ │ │ └── up.go │ ├── lang │ │ └── lang_spec.rb │ ├── config_spec.rb │ ├── storage │ │ └── db_spec.rb │ └── migrator_spec.rb └── spec_helper.rb ├── .rspec ├── Gemfile ├── .gitignore ├── lib ├── migrate │ ├── errors.rb │ ├── storage.rb │ ├── lang.rb │ ├── lang │ │ ├── lang.rb │ │ ├── javascript.rb │ │ ├── ruby.rb │ │ ├── python.rb │ │ ├── sql.rb │ │ └── go.rb │ ├── logger.rb │ ├── storage │ │ ├── mysql.rb │ │ ├── postgres.rb │ │ └── db.rb │ ├── conf.rb │ └── migrator.rb └── migrate.rb ├── .travis.yml ├── LICENSE ├── db-migrate.gemspec ├── Gemfile.lock ├── README.md └── bin └── migrate /spec/lib/fixtures/rb/up.rb: -------------------------------------------------------------------------------- 1 | puts "works" 2 | -------------------------------------------------------------------------------- /spec/lib/fixtures/sql/down.sql: -------------------------------------------------------------------------------- 1 | -- todo; 2 | -------------------------------------------------------------------------------- /spec/lib/fixtures/sql/up.sql: -------------------------------------------------------------------------------- 1 | -- todo; 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /spec/lib/fixtures/py/down.py: -------------------------------------------------------------------------------- 1 | print "works" 2 | -------------------------------------------------------------------------------- /spec/lib/fixtures/py/up.py: -------------------------------------------------------------------------------- 1 | print "works" 2 | -------------------------------------------------------------------------------- /spec/lib/fixtures/rb/down.rb: -------------------------------------------------------------------------------- 1 | puts "works" 2 | -------------------------------------------------------------------------------- /spec/lib/fixtures/js/up.js: -------------------------------------------------------------------------------- 1 | console.log("works"); 2 | -------------------------------------------------------------------------------- /spec/lib/fixtures/js/down.js: -------------------------------------------------------------------------------- 1 | console.log("works"); 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.conf 2 | *.config 3 | spec/lib/fixtures/v* 4 | .bundle/ 5 | vendor 6 | *.gem 7 | -------------------------------------------------------------------------------- /lib/migrate/errors.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | class VersionNotFound < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/migrate/storage.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | module Storage 3 | require_relative "./storage/db" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/lib/fixtures/go/down.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("works") 7 | } 8 | -------------------------------------------------------------------------------- /spec/lib/fixtures/go/up.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("works") 7 | } 8 | -------------------------------------------------------------------------------- /lib/migrate.rb: -------------------------------------------------------------------------------- 1 | require_relative "./migrate/logger" 2 | require_relative "./migrate/conf" 3 | require_relative "./migrate/storage" 4 | require_relative "./migrate/lang" 5 | require_relative "./migrate/migrator" 6 | require_relative "./migrate/errors" 7 | 8 | module Migrate 9 | end 10 | -------------------------------------------------------------------------------- /lib/migrate/lang.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | module Lang 3 | require_relative "./lang/lang" 4 | require_relative "./lang/sql" 5 | require_relative "./lang/javascript" 6 | require_relative "./lang/ruby" 7 | require_relative "./lang/go" 8 | require_relative "./lang/python" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/migrate/lang/lang.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | module Lang 3 | class Lang 4 | attr_reader :ext 5 | 6 | def create_migration(dir) 7 | raise "Implementation for creating new migration not found." 8 | end 9 | 10 | def exec_migration(dir, is_up) 11 | raise "Implementation for executing migration not found." 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.0.0 5 | - 2.1 6 | - 2.2 7 | 8 | script: "bundle exec rspec" 9 | 10 | addons: 11 | postgresql: "9.4" 12 | 13 | services: 14 | - mysql 15 | - postgresql 16 | 17 | before_script: 18 | - mysql -e 'create database migrate_test;' 19 | - psql -c 'create database migrate_test;' -U postgres 20 | - psql -c 'CREATE SCHEMA IF NOT EXISTS public;' -U postgres 21 | - psql -c 'SET search_path = public;' -U postgres 22 | -------------------------------------------------------------------------------- /lib/migrate/lang/javascript.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | module Lang 3 | class Javascript < Lang 4 | def initialize 5 | @ext = "js" 6 | end 7 | 8 | def create_migration(dir) 9 | File.open("#{dir}/up.#{@ext}", "w") do |f| 10 | f.puts "// Here goes JS for migration forward\n" 11 | end 12 | 13 | File.open("#{dir}/down.#{@ext}", "w") do |f| 14 | f.puts "// Here goes JS for migration backward\n" 15 | end 16 | end 17 | 18 | def exec_migration(dir, is_up) 19 | script = "#{dir}/#{is_up ? "up" : "down"}.#{@ext}" 20 | Log.info("Executing #{script}...") 21 | `node #{script}` 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/migrate/lang/ruby.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | module Lang 3 | class Ruby < Lang 4 | def initialize 5 | @ext = "rb" 6 | end 7 | 8 | def create_migration(dir) 9 | File.open("#{dir}/up.#{@ext}", "w") do |f| 10 | f.puts "# Here goes Ruby code for migration forward\n" 11 | end 12 | 13 | File.open("#{dir}/down.#{@ext}", "w") do |f| 14 | f.puts "# Here goes Ruby code for migration backward\n" 15 | end 16 | end 17 | 18 | def exec_migration(dir, is_up) 19 | script = "#{dir}/#{is_up ? "up" : "down"}.#{@ext}" 20 | Log.info("Executing #{script}...") 21 | `ruby #{script}` 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/migrate/lang/python.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | module Lang 3 | class Python < Lang 4 | def initialize 5 | @ext = "py" 6 | end 7 | 8 | def create_migration(dir) 9 | File.open("#{dir}/up.#{@ext}", "w") do |f| 10 | f.puts "# Here goes Python code for migration forward\n" 11 | end 12 | 13 | File.open("#{dir}/down.#{@ext}", "w") do |f| 14 | f.puts "# Here goes Python code for migration backward\n" 15 | end 16 | end 17 | 18 | def exec_migration(dir, is_up) 19 | script = "#{dir}/#{is_up ? "up" : "down"}.#{@ext}" 20 | Log.info("Executing #{script}...") 21 | `python #{script}` 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/migrate/lang/sql.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | module Lang 3 | class Sql < Lang 4 | def initialize(db) 5 | @db = db 6 | @ext = "sql" 7 | end 8 | 9 | def create_migration(dir) 10 | File.open("#{dir}/up.#{@ext}", "w") do |f| 11 | f.puts "-- Here goes SQL for migration forward\n" 12 | end 13 | 14 | File.open("#{dir}/down.#{@ext}", "w") do |f| 15 | f.puts "-- Here goes SQL for migration backward\n" 16 | end 17 | end 18 | 19 | def exec_migration(dir, is_up) 20 | script = "#{dir}/#{is_up ? "up" : "down"}.#{@ext}" 21 | Log.info("Executing #{script}...") 22 | @db.exec_sql(File.read(script)) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/migrate/lang/go.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | module Lang 3 | class Go < Lang 4 | def initialize 5 | @ext = "go" 6 | end 7 | 8 | def create_migration(dir) 9 | File.open("#{dir}/up.#{@ext}", "w") do |f| 10 | f.puts <<-eot 11 | package main 12 | 13 | func main() { 14 | // Here goes your Go migration forward 15 | } 16 | eot 17 | end 18 | 19 | File.open("#{dir}/down.#{@ext}", "w") do |f| 20 | f.puts <<-eot 21 | package main 22 | 23 | func main() { 24 | // Here goes your Go migration backward 25 | } 26 | eot 27 | end 28 | end 29 | 30 | def exec_migration(dir, is_up) 31 | script = "#{dir}/#{is_up ? "up" : "down"}.#{@ext}" 32 | Log.info("Executing #{script}...") 33 | `go run #{script}` 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/migrate/logger.rb: -------------------------------------------------------------------------------- 1 | require "colorize" 2 | 3 | module Migrate 4 | class Log 5 | @@debug = true 6 | 7 | def self.verbose(verbose) 8 | @@debug = verbose 9 | end 10 | 11 | def self.info(msg) 12 | return if not @@debug 13 | puts ("[INFO] " + msg).green 14 | end 15 | 16 | def self.warn(msg) 17 | return if not @@debug 18 | puts ("[WARN] " + msg).yellow 19 | end 20 | 21 | def self.error(msg, e=nil) 22 | return if not @@debug 23 | puts ("[ERRPR] " + msg + (e != nil ? " #{e.message}" : "")).red 24 | 25 | if e != nil 26 | puts e.backtrace 27 | end 28 | end 29 | 30 | def self.success(msg) 31 | return if not @@debug 32 | puts ("[SUCCESS] " + msg).blue 33 | end 34 | 35 | def self.version(msg) 36 | puts (" [VERSION] #{msg} ").colorize(:color => :white, :background => :blue) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ivan Pusic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /db-migrate.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'db-migrate' 3 | s.version = '0.2.2' 4 | s.licenses = ['MIT'] 5 | s.summary = "Tool for managing and executing your database migrations." 6 | s.description = "#{s.summary} It supports multiple databases and multiple languages for writing migration scripts." 7 | s.authors = ["Ivan Pusic"] 8 | s.email = 'pusic007@gmail.com' 9 | s.files = `git ls-files -z`.split("\x0") 10 | s.homepage = 'https://github.com/ivpusic/migrate' 11 | s.executables << 'migrate' 12 | 13 | # runtime deps 14 | s.add_runtime_dependency 'thor', ['0.19.1'] 15 | s.add_runtime_dependency 'highline', ['1.7.8'] 16 | s.add_runtime_dependency 'json', ['1.8.3'] 17 | s.add_runtime_dependency 'parseconfig', ['1.0.6'] 18 | s.add_runtime_dependency 'colorize', ['0.7.7'] 19 | s.add_runtime_dependency 'terminal-table', ['1.5.2'] 20 | s.add_runtime_dependency 'wannabe_bool', ['0.5.0'] 21 | 22 | # dev deps 23 | s.add_development_dependency 'rspec', ['3.4.0'] 24 | s.add_development_dependency 'pg', ['0.18.4'] 25 | s.add_development_dependency 'mysql2', ['0.4.2'] 26 | end 27 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | db-migrate (0.2.2) 5 | colorize (= 0.7.7) 6 | highline (= 1.7.8) 7 | json (= 1.8.3) 8 | parseconfig (= 1.0.6) 9 | terminal-table (= 1.5.2) 10 | thor (= 0.19.1) 11 | wannabe_bool (= 0.5.0) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | colorize (0.7.7) 17 | diff-lcs (1.2.5) 18 | highline (1.7.8) 19 | json (1.8.3) 20 | mysql2 (0.4.2) 21 | parseconfig (1.0.6) 22 | pg (0.18.4) 23 | rspec (3.4.0) 24 | rspec-core (~> 3.4.0) 25 | rspec-expectations (~> 3.4.0) 26 | rspec-mocks (~> 3.4.0) 27 | rspec-core (3.4.3) 28 | rspec-support (~> 3.4.0) 29 | rspec-expectations (3.4.0) 30 | diff-lcs (>= 1.2.0, < 2.0) 31 | rspec-support (~> 3.4.0) 32 | rspec-mocks (3.4.1) 33 | diff-lcs (>= 1.2.0, < 2.0) 34 | rspec-support (~> 3.4.0) 35 | rspec-support (3.4.1) 36 | terminal-table (1.5.2) 37 | thor (0.19.1) 38 | wannabe_bool (0.5.0) 39 | 40 | PLATFORMS 41 | ruby 42 | 43 | DEPENDENCIES 44 | db-migrate! 45 | mysql2 (= 0.4.2) 46 | pg (= 0.18.4) 47 | rspec (= 3.4.0) 48 | 49 | BUNDLED WITH 50 | 1.11.2 51 | -------------------------------------------------------------------------------- /spec/lib/lang/lang_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Lang" do 2 | langs = [ 3 | Lang::Go.new, 4 | Lang::Javascript.new, 5 | Lang::Ruby.new, 6 | Lang::Sql.new($dbs[0]), 7 | Lang::Python.new 8 | ] 9 | 10 | langs.each do |lang| 11 | dir = "spec/lib/fixtures/v01" 12 | 13 | after(:each) do 14 | if Dir.exist? dir 15 | Dir.glob("#{dir}/{up.*,down.*}").each do |file| 16 | File.delete(file) 17 | end 18 | Dir.rmdir dir 19 | end 20 | end 21 | 22 | it "#{lang.ext} should be able to create new migration" do 23 | Dir.mkdir dir 24 | lang.create_migration(dir) 25 | 26 | up = "#{dir}/up.#{lang.ext}" 27 | down = "#{dir}/down.#{lang.ext}" 28 | 29 | expect(File.exist? up).to be true 30 | expect(File.exist? down).to be true 31 | end 32 | 33 | context "#{lang.ext} when migrating up" do 34 | [true, false].each do |is_up| 35 | it "should be able to execute migration script" do 36 | lang_dir = "spec/lib/fixtures/#{lang.ext}" 37 | 38 | if lang.ext != "sql" 39 | out = lang.exec_migration(lang_dir, is_up) 40 | expect(out.strip).to eq("works") 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/migrate/storage/mysql.rb: -------------------------------------------------------------------------------- 1 | require 'mysql2' 2 | 3 | module Migrate 4 | module Storage 5 | class Mysql < DB 6 | def initialize(*args) 7 | super 8 | @conn = Mysql2::Client.new( 9 | :database => @config.database, 10 | :host => @config.host, 11 | :port => @config.port, 12 | :username => @config.user, 13 | :password => @config.password, 14 | ) 15 | end 16 | 17 | def create_tables 18 | Log.info("Creating version table") 19 | self.exec_sql <<-eos 20 | CREATE TABLE #{@config.version_info} 21 | ( 22 | version INT PRIMARY KEY NOT NULL AUTO_INCREMENT, 23 | description TEXT, 24 | created_date TIMESTAMP NOT NULL, 25 | last_up TIMESTAMP NULL, 26 | last_down TIMESTAMP NULL 27 | ); 28 | eos 29 | 30 | self.exec_sql <<-eos 31 | CREATE TABLE #{@config.version_number} ( 32 | version int(11) not null, 33 | PRIMARY KEY (version) 34 | ); 35 | eos 36 | 37 | self.exec_sql <<-eos 38 | INSERT INTO #{@config.version_number} VALUES(0); 39 | eos 40 | Log.success("Version table created") 41 | end 42 | 43 | def tables_exists? 44 | vi = self.exec_sql("SHOW TABLES LIKE '#{@config.version_info}'") 45 | vn = self.exec_sql("SHOW TABLES LIKE '#{@config.version_number}'") 46 | 47 | vi.length > 0 && vn.length > 0 48 | end 49 | 50 | def exec_sql(sql) 51 | results = [] 52 | result = @tx.query sql 53 | return [] if result == nil 54 | 55 | result.each do |row| 56 | results << row 57 | end 58 | end 59 | 60 | def has_tx 61 | @tx != nil 62 | end 63 | 64 | def tx 65 | if has_tx 66 | yield 67 | else 68 | begin 69 | @conn.query "BEGIN;" 70 | @tx = @conn 71 | yield 72 | @conn.query "COMMIT;" 73 | rescue Exception => e 74 | @conn.query "ROLLBACK;" 75 | raise e 76 | ensure 77 | @tx = nil 78 | end 79 | end 80 | end 81 | 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/migrate/storage/postgres.rb: -------------------------------------------------------------------------------- 1 | require "pg" 2 | require 'wannabe_bool' 3 | 4 | module Migrate 5 | module Storage 6 | class Postgres < DB 7 | def initialize(*args) 8 | super 9 | @conn = PG.connect({ 10 | dbname: @config.database, 11 | host: @config.host, 12 | user: @config.user, 13 | password: @config.password, 14 | }) 15 | end 16 | 17 | def tables_exists? 18 | vi = self.exec_sql <<-eos 19 | SELECT EXISTS ( 20 | SELECT 1 21 | FROM information_schema.tables 22 | WHERE table_name = '#{@config.version_info}' 23 | ); 24 | eos 25 | 26 | vn = self.exec_sql <<-eos 27 | SELECT EXISTS ( 28 | SELECT 1 29 | FROM information_schema.tables 30 | WHERE table_name = '#{@config.version_number}' 31 | ); 32 | eos 33 | 34 | vi[0]["exists"].to_b && vn[0]["exists"].to_b 35 | end 36 | 37 | def create_tables 38 | Log.info("Creating version table") 39 | self.exec_sql <<-eos 40 | CREATE TABLE #{@config.version_info} 41 | ( 42 | version SERIAL PRIMARY KEY NOT NULL, 43 | description TEXT, 44 | created_date TIMESTAMP WITH TIME ZONE NOT NULL, 45 | last_up TIMESTAMP WITH TIME ZONE, 46 | last_down TIMESTAMP WITH TIME ZONE 47 | ); 48 | CREATE UNIQUE INDEX #{@config.version_info}_version_uindex ON #{@config.version_info} (version); 49 | 50 | CREATE TABLE #{@config.version_number} 51 | ( 52 | version INT PRIMARY KEY NOT NULL 53 | ); 54 | 55 | INSERT INTO #{@config.version_number} VALUES(0); 56 | eos 57 | Log.success("Version table created") 58 | end 59 | 60 | 61 | def extract_version(results) 62 | if results && results.count > 0 63 | results[0]["version"] 64 | else 65 | raise VersionNotFound 66 | end 67 | end 68 | 69 | def exec_sql(sql) 70 | @tx.exec sql 71 | end 72 | 73 | def has_tx 74 | @tx != nil 75 | end 76 | 77 | def tx 78 | if has_tx 79 | yield 80 | else 81 | begin 82 | @conn.transaction do |tx| 83 | @tx = tx 84 | yield 85 | end 86 | ensure 87 | @tx = nil 88 | end 89 | end 90 | end 91 | 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/migrate/conf.rb: -------------------------------------------------------------------------------- 1 | require "parseconfig" 2 | 3 | module Migrate 4 | class Conf 5 | attr_reader :root 6 | 7 | def initialize(root, file) 8 | @root = root 9 | @file=file 10 | @file_path = "#{root}/#{file}" 11 | @loaded = false 12 | end 13 | 14 | def exists? 15 | File.exist? @file_path 16 | end 17 | 18 | def init(config) 19 | if not Dir.exist? @root 20 | Dir.mkdir @root 21 | end 22 | 23 | File.open(@file_path, "w") do |f| 24 | config.map do |key, value| 25 | f.puts "#{key}=#{value}\n" 26 | end 27 | end 28 | 29 | Log.success("Configuration file created. Location: `#{@file_path}`") 30 | end 31 | 32 | def load! 33 | Log.info("Loading configuration...") 34 | config = ParseConfig.new(@file_path) 35 | 36 | config.get_params.map do |param| 37 | value = nil 38 | env_var = config[param].match(/\$\{(.*)\}/) 39 | 40 | if env_var != nil 41 | value = ENV[env_var[1]] 42 | else 43 | value = config[param] 44 | end 45 | 46 | self.class.send(:attr_reader, param) 47 | instance_variable_set("@#{param}", value) 48 | end 49 | 50 | @loaded = true 51 | Log.success("Configuration loaded.") 52 | end 53 | 54 | def delete 55 | if File.exists? @file_path 56 | File.delete @file_path 57 | end 58 | rescue Exception => e 59 | Log.error("Error while removing configuration file.", e) 60 | exit 61 | end 62 | 63 | def get_db 64 | case @storage 65 | when "pg" 66 | if @pg == nil 67 | require_relative "./storage/postgres" 68 | @pg = Storage::Postgres.new(self) 69 | end 70 | 71 | @pg 72 | when "mysql" 73 | if @mysql == nil 74 | require_relative "./storage/mysql" 75 | @mysql = Storage::Mysql.new(self) 76 | end 77 | 78 | @mysql 79 | end 80 | end 81 | 82 | def get_lang 83 | case @lang 84 | when "sql" 85 | if @sql == nil 86 | @sql = Lang::Sql.new(get_db) 87 | end 88 | 89 | @sql 90 | when "javascript" 91 | if @javascript == nil 92 | @javascript = Lang::Javascript.new 93 | end 94 | 95 | @javascript 96 | when "ruby" 97 | if @ruby == nil 98 | @ruby = Lang::Ruby.new 99 | end 100 | 101 | @ruby 102 | when "go" 103 | if @go == nil 104 | @go = Lang::Go.new 105 | end 106 | 107 | @go 108 | when "python" 109 | if @python == nil 110 | @python = Lang::Python.new 111 | end 112 | 113 | @python 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/migrate" 2 | include Migrate 3 | 4 | Log.verbose(false) 5 | 6 | $config_base = { 7 | :lang => "sql", 8 | :database => "migrate_test", 9 | :host => "localhost", 10 | :version_info => "version_info", 11 | :version_number => "version_number" 12 | } 13 | 14 | $pg_config_hash = $config_base.merge({ 15 | :storage => "pg", 16 | :port => 5432, 17 | :user => "postgres", 18 | :password => "" 19 | }) 20 | 21 | def load_pg_config 22 | config = Conf.new("spec/lib/fixtures", "example_pg.config") 23 | config.init($pg_config_hash) 24 | config.load! 25 | return config 26 | end 27 | 28 | $mysql_config_hash = $config_base.merge({ 29 | :storage => "mysql", 30 | :port => 3306, 31 | :user => "root", 32 | :password => "" 33 | }) 34 | 35 | def load_mysql_config 36 | config = Conf.new("spec/lib/fixtures", "example_mysql.config") 37 | config.init($mysql_config_hash) 38 | config.load! 39 | return config 40 | end 41 | 42 | $dbs = [load_pg_config().get_db, load_mysql_config().get_db] 43 | $version_info = $config_base[:version_info] 44 | $version_number = $config_base[:version_number] 45 | 46 | def load_fixtures 47 | drop_fixtures 48 | $dbs.each do |db| 49 | if db.has_tx 50 | case db.type 51 | when "pg" 52 | db.exec_sql "ALTER SEQUENCE #{$version_info}_version_seq RESTART WITH 1" 53 | when "mysql" 54 | db.exec_sql "ALTER TABLE #{$version_info} AUTO_INCREMENT = 1" 55 | end 56 | 57 | db.exec_sql "INSERT INTO #{$version_info} (created_date) VALUES (now())" 58 | db.exec_sql "INSERT INTO #{$version_info} (created_date) VALUES (now())" 59 | db.exec_sql "INSERT INTO #{$version_info} (created_date) VALUES (now())" 60 | db.exec_sql "INSERT INTO #{$version_info} (created_date) VALUES (now())" 61 | db.exec_sql "INSERT INTO #{$version_info} (created_date) VALUES (now())" 62 | db.exec_sql "UPDATE #{$version_number} SET version=3" 63 | end 64 | end 65 | end 66 | 67 | def drop_fixtures 68 | $dbs.each do |db| 69 | if db.has_tx 70 | db.exec_sql "DELETE FROM #{$version_info}" 71 | end 72 | end 73 | end 74 | 75 | def create_tables 76 | $dbs.each do |db| 77 | db.tx do 78 | db.create_tables 79 | end 80 | end 81 | end 82 | 83 | def drop_tables 84 | $dbs.each do |db| 85 | db.tx do 86 | db.exec_sql "DROP TABLE IF EXISTS #{$version_info}" 87 | db.exec_sql "DROP TABLE IF EXISTS #{$version_number}" 88 | end 89 | end 90 | end 91 | 92 | RSpec.configure do |config| 93 | 94 | config.before(:all) do 95 | create_tables 96 | end 97 | 98 | config.before(:each) do 99 | load_fixtures 100 | end 101 | 102 | config.after(:each) do 103 | drop_fixtures 104 | end 105 | 106 | config.after(:all) do 107 | drop_tables 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/lib/config_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Conf" do 2 | let!(:fixtures) { "spec/lib/fixtures" } 3 | let!(:config) { Conf.new(fixtures, "example.config") } 4 | let!(:config_hash) { 5 | { 6 | host: "localhost", 7 | port: ("5432").to_i, 8 | database: "mydb", 9 | user: "postgres", 10 | password: "password", 11 | version_info: "version_info", 12 | version_number: "${MIGRATE_TEST_VERSION_NUMBER}" 13 | } 14 | } 15 | 16 | it "should create new instance" do 17 | expect(config).not_to eq(nil) 18 | end 19 | 20 | it "should save root" do 21 | expect(config.root).to eq("spec/lib/fixtures") 22 | end 23 | 24 | it "should not find file" do 25 | config = Conf.new(".", "custom_file.conf") 26 | expect(config.exists?).to eq(false) 27 | end 28 | 29 | it "should create config file" do 30 | config_path = "#{fixtures}/test.config" 31 | begin 32 | expect(File.exist? config_path).to be false 33 | 34 | config = Conf.new(fixtures, "test.config") 35 | config.init(config_hash) 36 | expect(config.exists?).to eq(true) 37 | expect(File.exist? config_path). to be true 38 | ensure 39 | if File.exist? config_path 40 | File.delete config_path 41 | end 42 | end 43 | end 44 | 45 | it "should load configuration" do 46 | ENV["MIGRATE_TEST_VERSION_NUMBER"] = "version_number" 47 | config.init(config_hash) 48 | config.load! 49 | expect(config.host).to eq("localhost") 50 | expect(config.port).to eq("5432") 51 | expect(config.database).to eq("mydb") 52 | expect(config.user).to eq("postgres") 53 | expect(config.password).to eq("password") 54 | expect(config.version_info).to eq("version_info") 55 | expect(config.version_number).to eq("version_number") 56 | end 57 | 58 | it "should remove configuration" do 59 | config_path = "#{fixtures}/test.config" 60 | begin 61 | expect(File.exist? config_path).to be false 62 | 63 | config = Conf.new(fixtures, "test.config") 64 | config.init(config_hash) 65 | expect(File.exist? config_path).to be true 66 | config.delete 67 | expect(File.exist? config_path).to be false 68 | ensure 69 | if File.exist? config_path 70 | File.delete config_path 71 | end 72 | end 73 | end 74 | 75 | [{type: "pg", cls: Storage::Postgres, conf: $pg_config_hash}, 76 | {type: "mysql", cls: Storage::Mysql, conf: $mysql_config_hash}].each do |storage| 77 | context storage[:type] do 78 | it "should be able to get database instance" do 79 | config.init(storage[:conf]) 80 | config.load! 81 | expect(config.get_db).to be_kind_of(storage[:cls]) 82 | end 83 | end 84 | end 85 | 86 | [{type: "go", cls: Lang::Go}, {type: "sql", cls: Lang::Sql}, 87 | {type: "ruby", cls: Lang::Ruby}, {type: "javascript", cls: Lang::Javascript}, 88 | {type: "python", cls: Lang::Python}].each do |lang| 89 | context lang[:type] do 90 | it "should be able to get language instance" do 91 | config_hash = $pg_config_hash.merge({ 92 | lang: lang[:type] 93 | }) 94 | 95 | config.init(config_hash) 96 | config.load! 97 | expect(config.get_lang).to be_kind_of(lang[:cls]) 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/lib/storage/db_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Storage" do 2 | $dbs.each do |db| 3 | around(:each) do |test| 4 | db.tx do 5 | test.run 6 | end 7 | end 8 | 9 | context db.type do 10 | it "should create new migration" do 11 | created = db.new_migration(200, "this is description") 12 | expect(created["version"].to_i).to eq(200) 13 | expect(created).not_to eq(nil) 14 | expect(db.exec_sql( 15 | "SELECT * FROM #{$version_info} WHERE version=#{created["version"]}")[0]) 16 | .to eq(created) 17 | end 18 | 19 | it "should list all migrations" do 20 | migrations = db.list_migrations(nil, nil) 21 | expect(migrations.count).to eq(5) 22 | expect(migrations[0].keys).to eq(["version", "description", "created_date", "last_up", "last_down"]) 23 | end 24 | 25 | it "should filter list of migrations" do 26 | select = "version,created_date" 27 | limit = 2 28 | 29 | migrations = db.list_migrations(select, limit) 30 | expect(migrations.count).to eq(limit) 31 | expect(migrations[0].keys).to eq(["version", "created_date"]) 32 | end 33 | 34 | describe "should get all migrations in range" do 35 | context "when doing up" do 36 | it do 37 | migrations = db.migrations_range(2, 4, true) 38 | expect(migrations.count).to eq(3) 39 | expect(migrations[0]["version"].to_s).to eq("2") 40 | expect(migrations[1]["version"].to_s).to eq("3") 41 | expect(migrations[2]["version"].to_s).to eq("4") 42 | end 43 | end 44 | 45 | context "when doing down" do 46 | it do 47 | migrations = db.migrations_range(2, 4, false) 48 | expect(migrations.count).to eq(3) 49 | expect(migrations[0]["version"].to_s).to eq("4") 50 | expect(migrations[1]["version"].to_s).to eq("3") 51 | expect(migrations[2]["version"].to_s).to eq("2") 52 | end 53 | end 54 | end 55 | 56 | it "should be able to check if version table already exists" do 57 | exists = db.tables_exists? 58 | expect(exists).to be true 59 | end 60 | 61 | it "should get lowest version" do 62 | version = db.lowest_version 63 | expect(version.to_s).to eq("1") 64 | end 65 | 66 | it "should get next version" do 67 | nxt = db.next_version 68 | expect(nxt.to_s).to eq("4") 69 | end 70 | 71 | it "should get current version" do 72 | current = db.current_version 73 | expect(current.to_s).to eq("3") 74 | end 75 | 76 | it "should get previous version" do 77 | prev = db.prev_version 78 | expect(prev.to_s).to eq("2") 79 | end 80 | 81 | it "should perform log up" do 82 | db.log_up(4) 83 | current = db.current_version 84 | expect(current.to_s).to eq("4") 85 | end 86 | 87 | it "should perform log down" do 88 | db.log_down("1") 89 | current = db.current_version 90 | expect(current.to_s).to eq("0") 91 | end 92 | 93 | it "should get migration" do 94 | migration = db.get_migration(4) 95 | expect(migration["version"].to_s).to eq("4") 96 | end 97 | 98 | it "should delete migration" do 99 | db.delete(4) 100 | expect{db.get_migration(4)}.to raise_error(VersionNotFound) 101 | end 102 | 103 | it "should exec sql" do 104 | result = db.exec_sql("SELECT * FROM #{$version_info}") 105 | expect(result.count).to eq(5) 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/lib/migrator_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Migrator" do 2 | $dbs.each do |db| 3 | config = db.config 4 | migrator = Migrator.new(config) 5 | 6 | around(:each) do |test| 7 | db.tx do 8 | test.run 9 | end 10 | end 11 | 12 | context db.type do 13 | it "should be able to initialize" do 14 | expect{Migrator.new(config)}.not_to raise_error(Exception) 15 | end 16 | 17 | it "should make new migration" do 18 | migration_dir = nil 19 | 20 | begin 21 | result = db.exec_sql("SELECT * FROM #{config.version_info} ORDER BY version DESC") 22 | version = db.extract_version result 23 | 24 | migration_dir = migrator.new("description") 25 | 26 | result = db.exec_sql("SELECT * FROM #{config.version_info} ORDER BY version DESC") 27 | new_version = db.extract_version result 28 | expect(new_version.to_i).to eq(version.to_i + 1) 29 | expect(Dir.exist? migration_dir).to be true 30 | ensure 31 | if migration_dir != nil 32 | Dir.glob("#{migration_dir}/{up.*,down.*}").each do |file| 33 | File.delete file 34 | end 35 | 36 | Dir.rmdir migration_dir 37 | end 38 | end 39 | end 40 | 41 | create_migration_dir = lambda do |version| 42 | migration = db.get_migration(version) 43 | migration_dir = migrator.migration_dir(migration) 44 | 45 | if not Dir.exist? migration_dir 46 | Dir.mkdir migration_dir 47 | config.get_lang().create_migration(migration_dir) 48 | end 49 | end 50 | 51 | it "should execute one up migration" do 52 | current = db.current_version().to_i 53 | create_migration_dir.call(current + 1) 54 | migrator.up 55 | 56 | expect(current.to_i + 1).to eq(db.current_version().to_i) 57 | end 58 | 59 | it "should execute multiple up migration" do 60 | current = db.current_version().to_i 61 | create_migration_dir.call(current + 1) 62 | create_migration_dir.call(current + 2) 63 | 64 | migrator.up(current + 2) 65 | expect(current.to_i + 2).to eq(db.current_version().to_i) 66 | end 67 | 68 | it "should execute all up migrations" do 69 | db.exec_sql "INSERT INTO #{$version_info} (created_date) VALUES (now())" 70 | db.exec_sql "INSERT INTO #{$version_info} (created_date) VALUES (now())" 71 | current = db.current_version().to_i 72 | create_migration_dir.call(current + 1) 73 | create_migration_dir.call(current + 2) 74 | create_migration_dir.call(current + 3) 75 | create_migration_dir.call(current + 4) 76 | migrator.up(nil, true) 77 | 78 | expect(current.to_i + 4).to eq(db.current_version().to_i) 79 | end 80 | 81 | it "should execute one down migration" do 82 | current = db.current_version().to_i 83 | create_migration_dir.call(current) 84 | 85 | migrator.down 86 | expect(current.to_i - 1).to eq(db.current_version().to_i) 87 | end 88 | 89 | it "should execute multiple up migration" do 90 | current = db.current_version().to_i 91 | create_migration_dir.call(current) 92 | create_migration_dir.call(current - 1) 93 | 94 | migrator.down(current - 2) 95 | expect(current.to_i - 2).to eq(db.current_version().to_i) 96 | end 97 | 98 | it "should get current version" do 99 | current = db.current_version().to_i 100 | expect(migrator.current_version().to_i).to eq(current) 101 | end 102 | 103 | it "should delete one version" do 104 | delete = db.current_version().to_i + 1 105 | migrator.delete(delete) 106 | expect{db.get_migration(delete)}.to raise_error(VersionNotFound) 107 | end 108 | 109 | it "should not delete current version" do 110 | current = db.current_version().to_i 111 | migrator.delete(current) 112 | expect(db.get_migration(current)["version"].to_i).to eq(current) 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # migrate 2 | [![Build Status](https://travis-ci.org/ivpusic/migrate.svg?branch=master)](https://travis-ci.org/ivpusic/migrate) 3 | 4 | Tool for managing and executing your database migrations. 5 | 6 | ## Installation 7 | 8 | ``` 9 | gem install db-migrate 10 | ``` 11 | 12 | If you are going to use for example Postgres with **migrate**, make sure that you have Postgresql server running. 13 | **migrate** is using lazy installation of database specific gems, so it will install `pg` or `mysql2` (if they are not installed already) gem when you choose database type. It is recommended that you install them manually ([officialy-supported-versions](https://github.com/ivpusic/migrate/blob/master/bin/migrate#L13)). 14 | 15 | ## Demo 16 | ![img](http://i.giphy.com/26tPaeasgQYU2mCoE.gif) 17 | 18 | ## How it works? 19 | It saves metadata about your migrations to database and uses that metadata for executing and creating new migrations. 20 | 21 | It supports multiple databases and multiple languages for executing migrations. 22 | 23 | #### Supported databases 24 | - PostgreSQL 25 | - MySQL 26 | 27 | #### Supported languages 28 | - SQL 29 | - Ruby 30 | - Python 31 | - Javascript (Node.js) 32 | - Go 33 | 34 | #### Additional features 35 | - ENV variables support in configuration file 36 | 37 | ## How to use it? 38 | 39 | ##### --help 40 | ``` 41 | Commands: 42 | migrate init # initialize tables and create config file if necessary 43 | migrate new [DESCRIPTION] # generate files for new migration 44 | migrate up # Upgrade database schema 45 | migrate down # Downgrade database schema 46 | migrate list # Show list of all migrations 47 | migrate delete [VERSION] # Will delete migration data 48 | migrate version # Show current version 49 | migrate help [COMMAND] # Describe available commands or one specific 50 | Options: 51 | -r, [--root=ROOT] # Sepcify migration root directory, where config file is located 52 | # Default: . 53 | -c, [--config=CONFIG] # Specify custom configuration file name 54 | # Default: migrate.conf 55 | ``` 56 | 57 | #### init 58 | First thing you have to do is to make initial configuration with **migrate init** command. 59 | 60 | **migrate** will look for file `migrate.conf`. If file exists, it will make configuration based on file contents. 61 | Example configuration file: 62 | 63 | ```bash 64 | # pg or mysql 65 | storage=pg 66 | # can be one of: sql, ruby, javascript, go, python 67 | lang=sql 68 | # db host 69 | host=localhost 70 | # db port 71 | port=5432 72 | # name of database to use 73 | database=mydb 74 | # db user 75 | user=myuser 76 | # db password 77 | password=${SOME_ENV_VARIABLE} 78 | # name of table where version information will be stored 79 | version_info=version_info_table_name 80 | # name of table where version number will be stored 81 | version_number=version_number_table_name 82 | ``` 83 | 84 | If configuration file does not exist, it will run interactive configuration file creation process. You will answer few questions about your database, and **migrate** will create configuration file for you. 85 | 86 | #### new 87 | This command will generate migration scripts for you based on your prefered language. 88 | 89 | You will get new directory in format `vXXX-YYYY`, where `XXX` is version number, and `YYY` is short description you provide. Inside generated directory there will be two files. `up.LANG` and `down.LANG`, where `LANG` is language you use for writing migration scripts. 90 | 91 | #### up 92 | When you are done with writing your `up` and `down` migration script, you can execute **migrate up** to run up migration script for new version. 93 | 94 | Running `up` without arguments will move for one version up. You can also execute multiple migrations in single call by providing `--to n` argument, where `n` is highest version where you want to navigate. 95 | 96 | If you want to run all remaining available migrations, you can pass `-a` flag to `up` command, and **migrate** will run all available migrations. 97 | 98 | #### down 99 | You can also use **migrate down** to go one version back. `down` comand also accepts `--to n` argument, but in this case `n` is lowest version where you want to navigate. 100 | 101 | #### version 102 | If you are asking yourself about current version, use **migrate version** to find out current version. 103 | 104 | #### delete 105 | If you don't need some migration, use **migrate delete n** to remove version `n`. 106 | 107 | #### list 108 | You can see list of your migrations by running **migrate list**. This command also provides some additional options for filtering results. 109 | 110 | ## Contributing 111 | - do ruby magic 112 | - write tests! 113 | - send pull request 114 | 115 | ## License 116 | *MIT* 117 | -------------------------------------------------------------------------------- /lib/migrate/migrator.rb: -------------------------------------------------------------------------------- 1 | module Migrate 2 | class Migrator 3 | def initialize(config) 4 | @config = config 5 | @db = config.get_db 6 | @lang = config.get_lang 7 | 8 | if @db == nil 9 | throw "Database connection not found." 10 | exit 11 | end 12 | 13 | if @lang == nil 14 | throw "Language not found." 15 | end 16 | end 17 | 18 | def init 19 | @db.tx do 20 | if @db.tables_exists? 21 | Log.info("Version tables already exist.") 22 | else 23 | @db.create_tables 24 | end 25 | 26 | self.recover 27 | end 28 | end 29 | 30 | def recover 31 | @db.tx do 32 | directory = @config.root 33 | migrations = Dir.entries(directory).select { |file| File.directory? File.join(directory, file)} 34 | migrations.each do |migration| 35 | match = migration.match(/v(\d*)-(.*)/i) 36 | if match != nil 37 | v, desc = match.captures 38 | unless @db.version_exists?(v) 39 | self.new(desc, v) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | 47 | def migration_dir(migration) 48 | date = DateTime.parse(migration["created_date"].to_s) 49 | "#{@config.root}/v#{migration["version"]}-#{migration["description"]}" 50 | end 51 | 52 | def new(desc, version=nil) 53 | @db.tx do 54 | Log.info("Creating new migration...") 55 | 56 | if version == nil 57 | version = @db.highest_version.to_i + 1 58 | end 59 | 60 | migration = @db.new_migration(version, desc) 61 | migration_dir = self.migration_dir(migration) 62 | 63 | if Dir.exists? migration_dir 64 | Log.info("Migration directory '#{migration_dir}' already exists.") 65 | else 66 | Dir.mkdir migration_dir 67 | @lang.create_migration(migration_dir) 68 | end 69 | 70 | Log.success("Migration for version #{migration["version"]} created.") 71 | migration_dir 72 | end 73 | rescue Exception => e 74 | Log.error("Error while creating new migration.", e) 75 | exit 76 | end 77 | 78 | # will execute single migration by running up or down script 79 | def exec_migration(migration, is_up) 80 | migration_dir = self.migration_dir(migration) 81 | result = @lang.exec_migration(migration_dir, is_up) 82 | if @lang.ext != "sql" 83 | puts result 84 | end 85 | 86 | Log.info("Updating current version number...") 87 | version = migration["version"] 88 | is_up ? @db.log_up(version) : @db.log_down(version) 89 | end 90 | 91 | # will execute range of migrations 92 | def exec_migrations(is_up=true) 93 | Log.info("Executing migrations...") 94 | migrations = yield @db.current_version 95 | 96 | if migrations.count == 0 97 | Log.warn("Migrations not found") 98 | return 99 | end 100 | 101 | migrations.each do |migration| 102 | self.exec_migration(migration, is_up) 103 | end 104 | Log.success("Migrations executed. Current version: #{@db.current_version}") 105 | end 106 | 107 | def up(to_version=nil, all=false) 108 | @db.tx do 109 | self.exec_migrations do |last_version| 110 | if all 111 | @db.migrations_from(@db.next_version) 112 | else 113 | new_version = @db.next_version 114 | if to_version == nil 115 | to_version = new_version 116 | end 117 | 118 | @db.migrations_range(new_version, to_version, true) 119 | end 120 | end 121 | end 122 | end 123 | 124 | def down(to_version=nil) 125 | @db.tx do 126 | self.exec_migrations(false) do |current_version| 127 | if current_version == 0 128 | raise VersionNotFound 129 | end 130 | 131 | if to_version == nil 132 | to_version = current_version 133 | else 134 | to_version = to_version.to_i + 1 135 | end 136 | 137 | @db.migrations_range(to_version, current_version, false) 138 | end 139 | end 140 | end 141 | 142 | def current_version 143 | @db.tx do 144 | return @db.current_version 145 | end 146 | end 147 | 148 | def delete(version) 149 | @db.tx do 150 | Log.info("Removing migration data...") 151 | 152 | if @db.current_version.to_i == version 153 | return Log.error("Cannot remove current version.") 154 | end 155 | 156 | dir = self.migration_dir(@db.get_migration(version)) 157 | @db.delete version 158 | 159 | if Dir.exist? dir 160 | File.delete "#{dir}/up.sql" 161 | File.delete "#{dir}/down.sql" 162 | Dir.rmdir dir 163 | end 164 | 165 | Log.success("Migration data removed.") 166 | end 167 | end 168 | 169 | def list(select, limit) 170 | @db.tx do 171 | migrations = @db.list_migrations(select, limit) 172 | if not migrations.any? 173 | return Log.info("Migrations not found") 174 | end 175 | 176 | @db.print(migrations, "Migrations") 177 | end 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/migrate/storage/db.rb: -------------------------------------------------------------------------------- 1 | require 'terminal-table' 2 | 3 | module Migrate 4 | module Storage 5 | class DB 6 | attr_reader :config 7 | 8 | def initialize(config) 9 | @config = config 10 | end 11 | 12 | def type 13 | @config.storage 14 | end 15 | 16 | def new_migration(version=0, description="") 17 | self.exec_sql <<-eos 18 | INSERT INTO #{@config.version_info} (version, description, created_date) 19 | VALUES(#{version}, '#{description}', now()) 20 | eos 21 | 22 | res = self.exec_sql <<-eos 23 | SELECT * FROM #{@config.version_info} ORDER BY version DESC LIMIT 1 24 | eos 25 | res[0] 26 | end 27 | 28 | def list_migrations(selects, limit) 29 | self.exec_sql <<-eos 30 | SELECT #{(selects == nil ? "*" : selects)} FROM #{@config.version_info} 31 | ORDER BY last_up, version 32 | #{limit != nil ? "LIMIT #{limit}" : ""} 33 | eos 34 | end 35 | 36 | def migrations_range(from, to, is_up) 37 | self.exec_sql <<-eos 38 | SELECT * FROM #{@config.version_info} 39 | WHERE version >= #{from} AND version <= #{to} 40 | ORDER BY version #{!is_up ? "DESC" : ""} 41 | eos 42 | end 43 | 44 | def migrations_from(from) 45 | self.exec_sql <<-eos 46 | SELECT * FROM #{@config.version_info} 47 | WHERE version >= #{from} 48 | ORDER BY version 49 | eos 50 | end 51 | 52 | def extract_version(results) 53 | if results && results.count > 0 54 | results[0]["version"] 55 | else 56 | raise VersionNotFound 57 | end 58 | end 59 | 60 | def lowest_version 61 | self.extract_version self.exec_sql <<-eos 62 | SELECT version FROM #{@config.version_info} 63 | ORDER BY version 64 | LIMIT 1 65 | eos 66 | end 67 | 68 | def highest_version 69 | self.extract_version self.exec_sql <<-eos 70 | SELECT version FROM #{@config.version_info} 71 | ORDER BY version DESC 72 | LIMIT 1 73 | eos 74 | rescue VersionNotFound => e 75 | 0 76 | end 77 | 78 | def next_version 79 | self.extract_version self.exec_sql <<-eos 80 | SELECT version FROM #{@config.version_info} 81 | WHERE version > (SELECT version FROM #{@config.version_number} LIMIT 1) 82 | ORDER BY version 83 | LIMIT 1 84 | eos 85 | end 86 | 87 | def current_version 88 | self.extract_version self.exec_sql <<-eos 89 | SELECT * FROM #{config.version_number} 90 | LIMIT 1 91 | eos 92 | end 93 | 94 | def prev_version 95 | self.extract_version self.exec_sql <<-eos 96 | SELECT version FROM #{@config.version_info} 97 | WHERE version < (SELECT version FROM #{@config.version_number} LIMIT 1) 98 | ORDER BY version DESC 99 | LIMIT 1 100 | eos 101 | end 102 | 103 | def log_up(version) 104 | self.exec_sql "UPDATE #{@config.version_info} SET last_up=now() WHERE version=#{version}" 105 | self.exec_sql "UPDATE #{@config.version_number} SET version=#{version}" 106 | end 107 | 108 | def log_down(version) 109 | self.exec_sql "UPDATE #{@config.version_info} SET last_down=now() WHERE version=#{version}" 110 | 111 | lowest_version = self.lowest_version 112 | version_to_save = lowest_version.to_i < version.to_i ? self.prev_version().to_i : 0 113 | self.exec_sql "UPDATE #{@config.version_number} SET version=#{version_to_save}" 114 | end 115 | 116 | def get_migration(version) 117 | res = self.exec_sql "SELECT * FROM #{@config.version_info} WHERE version=#{version}" 118 | if res && res.count > 0 119 | res[0] 120 | else 121 | raise VersionNotFound 122 | end 123 | end 124 | 125 | def version_exists?(version) 126 | self.get_migration(version) 127 | true 128 | rescue VersionNotFound 129 | false 130 | end 131 | 132 | def delete(version) 133 | self.exec_sql "DELETE FROM #{@config.version_info} WHERE version=#{version}" 134 | end 135 | 136 | def print(results, title="") 137 | rows = [] 138 | headings = results[0].keys 139 | 140 | results.each do |result| 141 | row = [] 142 | result.each do |column, value| 143 | if column == "description" 144 | if value.length > 70 145 | value = value.scan(/.{1,70}/).join("\n") 146 | end 147 | end 148 | 149 | row << value 150 | end 151 | rows << row 152 | end 153 | 154 | table = Terminal::Table.new :headings => headings, :rows => rows 155 | table.title = title 156 | puts table 157 | end 158 | 159 | # Will create database model used by tool 160 | def create_tables 161 | raise "Implementation for creating tables not found" 162 | end 163 | 164 | def tables_exists? 165 | raise "Implementation for checking if version tables already exists not found" 166 | end 167 | 168 | # Executes SQL 169 | def exec_sql(sql) 170 | raise "Implementation for executing SQL script not found" 171 | end 172 | 173 | # Creates new transaction. Should accept block. 174 | def tx 175 | raise "Implementation for starting new transaction not found" 176 | end 177 | end 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /bin/migrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "thor" 4 | require "json" 5 | require "highline" 6 | require 'wannabe_bool' 7 | require_relative "../lib/migrate" 8 | 9 | include Migrate 10 | $asker = HighLine.new 11 | 12 | class CLI < Thor 13 | @@pg_version = "0.18.4" 14 | @@mysql_version = "0.4.2" 15 | 16 | method_option :root, { 17 | :aliases => "-r", 18 | :default => ".", 19 | :desc => "Sepcify migration root directory, where config file is located" 20 | } 21 | method_option :config, { 22 | :aliases => "-c", 23 | :default => "migrate.conf", 24 | :desc => "Specify custom configuration file name" 25 | } 26 | def initialize(*args) 27 | super 28 | @config = Conf.new(options["root"], options["config"]) 29 | init_invoked = ARGV.length > 0 && ARGV[0] == "init" 30 | 31 | if @config.exists? 32 | @config.load! 33 | CLI.install_deps(@config.storage) 34 | @migrator = Migrator.new(@config) 35 | elsif not init_invoked 36 | Log.error("Configuration not found in `#{Pathname.new(@config.root).expand_path}`. " \ 37 | "Make sure you are in right directory or " \ 38 | "run `migrate init` to create configuration.") 39 | exit 40 | end 41 | end 42 | 43 | def self.install_deps(storage) 44 | install_dep = "" 45 | case storage 46 | when "pg" 47 | if `gem list -i pg`.to_b != true 48 | install_dep = "gem install pg -v #{@@pg_version}" 49 | Log.info "pg package not installed. Will install it." 50 | end 51 | when "mysql" 52 | if `gem list -i mysql2`.to_b != "true" 53 | install_dep = "gem install mysql2 -v #{@@mysql_version}" 54 | Log.info "mysql2 package not installed. Will install it." 55 | end 56 | end 57 | 58 | unless install_dep.empty? 59 | Log.info "Running '#{install_dep}'" 60 | unless system(install_dep) 61 | raise "Wrror while install dependencies. Please try to run '#{install_dep}' manually and try running 'init' again." 62 | else 63 | Log.info "Missing dependencies installed" 64 | end 65 | end 66 | end 67 | 68 | desc "init", "make configuration file" 69 | def init 70 | generated_config = false 71 | 72 | if @migrator == nil 73 | Log.info("Creating configuration...") 74 | generated_config = true 75 | 76 | storage = nil 77 | $asker.choose do |menu| 78 | menu.prompt = "Which database do you prefer?" 79 | 80 | menu.choice(:mysql) { storage = "mysql" } 81 | menu.choices(:pg) { storage = "pg" } 82 | end 83 | 84 | CLI.install_deps(storage) 85 | 86 | db_defaults = case storage 87 | when "mysql" 88 | { :port => "3306", :user => "root" } 89 | when "pg" 90 | { :port => "5432", :user => "postgres" } 91 | end 92 | 93 | lang = nil 94 | $asker.choose do |menu| 95 | menu.prompt = "What language would you like use for your migration scripts?" 96 | 97 | menu.choice(:sql) { lang = "sql" } 98 | menu.choices(:ruby) { lang = "ruby" } 99 | menu.choice(:javascript) { lang = "javascript" } 100 | menu.choice(:go) { lang = "go" } 101 | menu.choice(:python) { lang = "python" } 102 | end 103 | 104 | config = { 105 | storage: storage, 106 | lang: lang, 107 | host: $asker.ask("Host: ") {|q| q.default = "localhost"}, 108 | port: ($asker.ask("Port: ") {|q| q.default = db_defaults[:port]}).to_i, 109 | database: $asker.ask("Database Name: ") {|q| q.default = "mydb"}, 110 | user: $asker.ask("User: ") {|q| q.default = db_defaults[:user]}, 111 | password: $asker.ask("Password: ") {|q| q.echo = "x"}, 112 | version_info: $asker.ask("Version info table: ") {|q| q.default = "version_info"}, 113 | version_number: $asker.ask("Version number table: ") {|q| q.default = "version_number"} 114 | } 115 | 116 | @config.init(config) 117 | @config.load! 118 | @migrator = Migrator.new(@config) 119 | end 120 | 121 | @migrator.init 122 | @migrator.recover 123 | rescue Exception => e 124 | Log.error("Error while initialization.", e) 125 | if generated_config 126 | @config.delete 127 | end 128 | exit 129 | end 130 | 131 | desc "new [DESCRIPTION]", "generate files for new migration" 132 | def new(description="migration") 133 | @migrator.new(description) 134 | end 135 | 136 | desc "up", "Upgrade database schema" 137 | option :to, :aliases => "-t", :desc => "Upgrade to the version" 138 | option :all, :aliases => "-a", :type => :boolean, :default => false, :desc => "Execute all up migrations" 139 | def up 140 | @migrator.up(options[:to], options[:all]) 141 | rescue VersionNotFound => e 142 | Log.error("Next version not found.") 143 | rescue Exception => e 144 | Log.error("Error while migrating up.", e) 145 | end 146 | 147 | desc "down [TO_VERSION]", "Downgrade database schema" 148 | option :to, :aliases => "-t", :desc => "Downgrade back to the version" 149 | def down 150 | @migrator.down(options[:to]) 151 | rescue VersionNotFound => e 152 | Log.error("Previous version not found.") 153 | rescue Exception => e 154 | Log.error("Error while migrating down.", e) 155 | end 156 | 157 | desc "list", "Show list of all migrations" 158 | option :limit, :aliases => "-l", :desc => "Limit results" 159 | option :select, :aliases => "-s", :desc => "Columns to select" 160 | def list() 161 | @migrator.list(options[:select], options[:limit]) 162 | end 163 | 164 | desc "delete [VERSION]", "Will delete migration data" 165 | def delete(version) 166 | @migrator.delete(version.to_i) 167 | rescue VersionNotFound 168 | Log.error("Version not found.") 169 | rescue Exception => e 170 | Log.error("Error while removing migration.", e) 171 | end 172 | 173 | desc "version", "Show current version" 174 | def version() 175 | Log.version(@migrator.current_version()) 176 | end 177 | end 178 | 179 | CLI.start 180 | --------------------------------------------------------------------------------