pt-online-migration 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ *.rb diff=ruby
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+ gem 'debugger'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 LeadKarma LLC
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,108 @@
1
+ # PtOnlineMigration
2
+
3
+ Schema changes tend to lock tables, which can be unacceptable for your production
4
+ database. The wonderful people at Percona have developed a tool which helps you
5
+ avoid this problem on MySQL databases.
6
+
7
+ PTOnlineMigration patches ActiveRecord to have the option of altering tables through
8
+ the pt-online-schema-change command.
9
+
10
+ It is highly recommended that you study up on said command:
11
+ http://www.percona.com/doc/percona-toolkit/2.2/pt-online-schema-change.html
12
+
13
+ This gem depends on the Percona Toolkit, and therefore only works with MySQL.
14
+
15
+ ## Installation
16
+
17
+ Download/install the percona toolkit from their downloads page:
18
+ http://www.percona.com/downloads/percona-toolkit/
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ gem 'pt-online-migration'
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install pt-online-migration
31
+
32
+ ## Usage
33
+
34
+ The syntax is very similar to `change_table`
35
+
36
+ e.g.
37
+
38
+ ```ruby
39
+ class SimpleAlterFoo < ActiveRecord::Migration
40
+ def up
41
+ online_alter_table :foo_table, :execute do |t|
42
+ t.integer :new_column_name
43
+ end
44
+ end
45
+
46
+ def down
47
+ online_alter_table :foo_table, :execute do |t|
48
+ t.remove :new_column_name
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ or
55
+
56
+ ```ruby
57
+ class ComplexAlterFoo < ActiveRecord::Migration
58
+ def up
59
+ online_alter_table :foo_table, :execute, :database => 'foo_database', :critical_load => 'Threads_running:50' do |t|
60
+ t.integer :new_column_name, :another_new_column_name, :limit => 7
61
+ t.decimal :new_column_with_more_options, :precision => 5, :scale => 3
62
+ t.change :foo_column, :boolean, :null => false
63
+ t.rename :bar_column, :baz_column, :string, :limit => 140
64
+ t.index :foo_column, :unique => true, :name => 'foo_index'
65
+ end
66
+ end
67
+
68
+ def down
69
+ online_alter_table :foo_table, :execute, :database => 'foo_database', :critical_load => 'Threads_running:50' do |t|
70
+ t.remove :new_column_name, :another_new_column_name, :new_column_with_more_options
71
+ t.change :foo_column, :string
72
+ t.rename :baz_column, :bar_column, :string
73
+ t.remove_index :name => 'foo_index'
74
+ end
75
+ end
76
+ end
77
+ ```
78
+
79
+ but not
80
+
81
+ ```ruby
82
+ class FailAlterFoo < ActiveRecord::Migration
83
+ def change
84
+ online_alter_table :foo_table, :execute do |t|
85
+ t.integer :new_column_name
86
+ end
87
+ end
88
+ end
89
+ ```
90
+
91
+ Change is not supported.
92
+
93
+ The major difference is that the online_alter_table method takes a few new parameters.
94
+ Without the symbol :execute, pt-online-schema-change will perform a dry-run and no actual schema change will be made.
95
+ The new method also takes a hash of options which are given to the pt-online-schema-change
96
+ command itself. Note in the complex example above a database is specified. If you
97
+ don't specify a database `ActiveRecord::Base.connection.current_database` is assumed.
98
+
99
+ For a list of accepted options check out the percona-toolkit documention:
100
+ http://www.percona.com/doc/percona-toolkit/2.2/pt-online-schema-change.html
101
+
102
+ ## Contributing
103
+
104
+ 1. Fork it
105
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
106
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
107
+ 4. Push to the branch (`git push origin my-new-feature`)
108
+ 5. Create new Pull Request
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake.application.options.suppress_backtrace_pattern = /.*/
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.pattern = 'test/*_test.rb'
8
+ end
9
+
10
+ desc "Run tests"
11
+ task :default => :test
@@ -0,0 +1,44 @@
1
+ require "pt_online_migration/version"
2
+ require "pt_online_migration/pt_command_builder"
3
+
4
+ module PtOnlineMigration
5
+
6
+ class PtOnlineMigrationError < ActiveRecord::ActiveRecordError
7
+ end
8
+
9
+ class ActiveRecord::Migration
10
+ alias_method :orig_announce, :announce
11
+
12
+ def announce(message)
13
+ new_message = message
14
+ if @is_online_schema_change and message =~ /^(migrated|reverted)/
15
+ if @executed
16
+ new_message = 'pt-online-schema-change executed, %s' % message
17
+ else
18
+ new_message = 'pt-online-schema-change dry-run complete %s' % message.split(' ')[1]
19
+ end
20
+ end
21
+ orig_announce new_message
22
+ end
23
+
24
+
25
+ def online_alter_table(*args)
26
+ raise "online_alter_table not supported within 'change' migration" if caller[0][/`.*'/][1..-2] == 'change'
27
+
28
+ @is_online_schema_change = true
29
+
30
+ host, username, password = Rails.configuration.database_configuration[Rails.env].values_at('host', 'username', 'password')
31
+ default_options = {:host => host, :username => username, :password => password, :database => connection.current_database}
32
+ options = default_options.merge(args.extract_options!.symbolize_keys)
33
+ pt_command = PTCommandBuilder.new(args[0], options, args[1] == :execute)
34
+ @executed = args[1] == :execute
35
+ yield pt_command
36
+ puts pt_command.command
37
+ system("nohup #{pt_command.command} >#{@name}_#{pt_command.table_name}.nohup.out 2>&1")
38
+ unless $?.success?
39
+ raise PtOnlineMigrationError.new
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,113 @@
1
+
2
+ module PtOnlineMigration
3
+
4
+ class PTCommandBuilder
5
+
6
+ def initialize(table_name, options, execute)
7
+ @table_name = table_name
8
+ execute_clause = execute ? '--execute' : '--dry-run'
9
+ @cmd_prefix = ["pt-online-schema-change h=#{options.delete(:host)},u=#{options.delete(:username)},p=#{options.delete(:password)},D=#{options.delete(:database)},t=#{table_name} #{execute_clause} --print"]
10
+ @pt_options = options
11
+ @alter_statements = []
12
+ end
13
+
14
+
15
+ def table_name
16
+ @table_name
17
+ end
18
+
19
+
20
+ %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |type|
21
+ define_method type do |*args|
22
+ options = args.extract_options!
23
+ args.each do |name|
24
+ add_column(name, type.to_sym, options)
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+ def add_column(name, type, options)
31
+ alter_statement
32
+ definition = column_definition(type, options)
33
+ @alter_statements.push "add column #{name.to_s} #{definition}"
34
+ end
35
+
36
+
37
+ def index(columns, options = {})
38
+ alter_statement
39
+ columns = Array(columns)
40
+ options[:name] ||= "#{@table_name}_#{columns.join('_')}_index"
41
+ @alter_statements.push "add#{' unique' if options[:unique]} index #{options[:name].to_s} (#{columns.join(', ')})"
42
+ end
43
+
44
+
45
+ def rename(old_name, new_name, type, options = {})
46
+ alter_statement
47
+ @cmd_prefix.push '--no-check-alter'
48
+ definition = column_definition(type, options)
49
+ @alter_statements.push "change column #{old_name} #{new_name} #{definition}"
50
+ end
51
+
52
+
53
+ def command
54
+ options = ''
55
+ @pt_options.map do |k, v|
56
+ options += " --#{k.to_s.gsub('_', '-')} '#{v}'"
57
+ end
58
+
59
+ return "#{@cmd_prefix.join(' ') + options} #{@modification_type} '#{@alter_statements.join(', ')}'"
60
+ end
61
+
62
+
63
+ def change(name, type, options = {})
64
+ alter_statement
65
+ @alter_statements.push "modify column #{name.to_s} #{column_definition(type, options)}"
66
+ end
67
+
68
+
69
+ def remove(*column_names)
70
+ alter_statement
71
+ @alter_statements.concat(column_names.map {|name| "drop column #{name.to_s}"})
72
+ end
73
+
74
+
75
+ def remove_index(options)
76
+ alter_statement
77
+ options = {:column => options} if options.class == Symbol
78
+ index = index_name(options)
79
+ @alter_statements.push "drop index #{index}"
80
+ end
81
+
82
+ private
83
+
84
+ def alter_statement
85
+ @modification_type ||= '--alter'
86
+ end
87
+
88
+
89
+ def column_definition(type, options)
90
+ default_options = {:default => :no_default, :precision => 1, :null => true, :scale => 0}
91
+ options = default_options.merge(options)
92
+
93
+ column_definition = [ActiveRecord::Base.connection.type_to_sql(type, options[:limit], options[:precision], options[:scale])]
94
+
95
+ column_definition.push 'not null' if options[:null] == false
96
+ options[:default] ||= 'null'
97
+ column_definition.push "default #{options[:default]}" unless options[:default] == :no_default
98
+ column_definition.push 'auto_increment' if options[:auto_increment]
99
+ column_definition.push 'first' if options[:first]
100
+ column_definition.push "after #{options[:after]}" if options[:after]
101
+
102
+ return column_definition.join(' ')
103
+ end
104
+
105
+
106
+ def index_name(options)
107
+ return "#{@table_name}_#{options[:column]}_index" if options[:column]
108
+ return "#{@table_name}_#{options[:columns].join('_')}_index" if options[:columns]
109
+ return options[:name] if options[:name]
110
+ raise ArgumentError, "unable to determine index name from #{options.inspect}"
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,3 @@
1
+ module PtOnlineMigration
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pt_online_migration/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pt-online-migration"
8
+ spec.version = PtOnlineMigration::VERSION
9
+ spec.authors = ['LeadKarma, LLC']
10
+ spec.email = ['support@leadkarma.com']
11
+ spec.description = %q{active record migration wrapper for pt-online-schema-change cli command}
12
+ spec.summary = %q{online schema migrations for mysql}
13
+ spec.homepage = "http://www.leadkarma.com/"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'activerecord'
22
+ spec.add_dependency 'mysql2'
23
+ spec.add_development_dependency 'bundler', '~> 1.3'
24
+ spec.add_development_dependency 'mocha'
25
+ spec.add_development_dependency 'rake'
26
+ end
@@ -0,0 +1,129 @@
1
+ require 'active_record'
2
+ require 'active_record/connection_adapters/mysql2_adapter'
3
+ require 'pt_online_migration'
4
+ require 'test/unit'
5
+ require 'mysql2'
6
+ require 'mocha/setup'
7
+
8
+ Rails = Class.new
9
+
10
+ class PTOnlineMigrationTest < Test::Unit::TestCase
11
+
12
+ def setup
13
+ @test_migration = ActiveRecord::Migration.new
14
+
15
+ @test_migration.class.class_eval do
16
+ attr_accessor :cmd, :puts_message
17
+
18
+ def system(command)
19
+ self.cmd = command
20
+ end
21
+
22
+ def puts(message)
23
+ self.puts_message = message
24
+ end
25
+ end
26
+
27
+ Mysql2::Client.expects(:new).at_least_once.returns(nil)
28
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.any_instance.expects(:configure_connection).at_least_once.returns(true)
29
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.any_instance.expects(:current_database).returns('stub_db')
30
+ Rails.stubs(:configuration => stub(:database_configuration => { 'test' => {'host' => 'stub_host', 'username' => 'stub_user', 'password' => 'stub_password'}}))
31
+ Rails.expects(:env).returns('test').at_least_once
32
+ ActiveRecord::Base.establish_connection({
33
+ "adapter"=>"mysql2",
34
+ "database"=>"stub_db",
35
+ })
36
+ end
37
+
38
+
39
+ def test_complicated_migration
40
+ @test_migration.online_alter_table :foo_table, :execute, :host => 'bar_host', :database => 'foo_database', :username => 'baz_user', :password => 'biz_password', :critical_load => 'Threads_running:50' do |t|
41
+ t.integer :new_column, :another_new_column, :limit => 7
42
+ t.decimal :new_column_with_more_options, :precision => 5, :scale => 3, :default => nil
43
+ t.change :foo_column, :boolean, :null => false
44
+ t.rename :bar_column, :baz_column, :string, :limit => 140
45
+ t.index :foo_column, :unique => true, :name => 'foo_index'
46
+ end
47
+
48
+ expected_pt_command = [
49
+ 'pt-online-schema-change h=bar_host,u=baz_user,p=biz_password,D=foo_database,t=foo_table --execute --print',
50
+ "--no-check-alter --critical-load 'Threads_running:50' --alter 'add column",
51
+ 'new_column bigint, add column another_new_column bigint, add column',
52
+ 'new_column_with_more_options decimal(5,3) default null, modify column foo_column tinyint(1)',
53
+ 'not null, change column bar_column baz_column varchar(140), add unique index',
54
+ "foo_index (foo_column)'"
55
+ ]
56
+
57
+ expected = "nohup #{expected_pt_command.join(' ')} >#{@test_migration.name}_foo_table.nohup.out 2>&1"
58
+ assert_equal expected, @test_migration.cmd
59
+ end
60
+
61
+
62
+ def test_remove_migration
63
+ @test_migration.online_alter_table :foo_table, :execute, :host => 'bar_host', :username => 'baz_user', :password => 'biz_password', :database => 'foo_database', :critical_load => 'Threads_running:50' do |t|
64
+ t.remove :new_column, :another_new_column, :new_column_with_more_options
65
+ t.change :foo_column, :string
66
+ t.rename :baz_column, :bar_column, :string
67
+ t.remove_index :name => 'foo_index'
68
+ end
69
+
70
+ expected_pt_command = [
71
+ "pt-online-schema-change h=bar_host,u=baz_user,p=biz_password,D=foo_database,t=foo_table --execute --print --no-check-alter --critical-load 'Threads_running:50' --alter",
72
+ "'drop column new_column, drop column another_new_column, drop column new_column_with_more_options,",
73
+ "modify column foo_column varchar(255), change column baz_column bar_column varchar(255), drop index foo_index'"
74
+ ]
75
+
76
+ expected = "nohup #{expected_pt_command.join(' ')} >#{@test_migration.name}_foo_table.nohup.out 2>&1"
77
+ assert_equal expected, @test_migration.cmd
78
+ end
79
+
80
+
81
+ def test_simple_migration
82
+ @test_migration.online_alter_table :foo_table, :execute do |t|
83
+ t.integer :new_column_name
84
+ end
85
+
86
+ expected_pt_command = "pt-online-schema-change h=stub_host,u=stub_user,p=stub_password,D=stub_db,t=foo_table --execute --print --alter 'add column new_column_name int(11)'"
87
+ expected = "nohup #{expected_pt_command} >#{@test_migration.name}_foo_table.nohup.out 2>&1"
88
+ assert_equal expected, @test_migration.cmd
89
+ end
90
+
91
+
92
+ def test_simple_dry_run
93
+ @test_migration.online_alter_table :foo_table do |t|
94
+ t.integer :new_column_name
95
+ end
96
+
97
+ expected_pt_command = "pt-online-schema-change h=stub_host,u=stub_user,p=stub_password,D=stub_db,t=foo_table --dry-run --print --alter 'add column new_column_name int(11)'"
98
+ expected = "nohup #{expected_pt_command} >#{@test_migration.name}_foo_table.nohup.out 2>&1"
99
+ assert_equal expected, @test_migration.cmd
100
+ end
101
+
102
+
103
+ def test_announce_pass_through
104
+ @test_migration.online_alter_table :foo_table do |t|
105
+ t.integer :new_column_name
106
+ end
107
+ @test_migration.announce 'this should pass through'
108
+ assert_nil @test_migration.puts_message =~ /pt-online-schema-change/
109
+ assert_not_nil @test_migration.puts_message =~ /this should pass through/
110
+ end
111
+
112
+
113
+ def test_announce_migrated
114
+ @test_migration.online_alter_table :foo_table do |t|
115
+ t.integer :new_column_name
116
+ end
117
+ @test_migration.announce 'migrated, this should get cut off'
118
+ assert_not_nil @test_migration.puts_message =~ /pt-online-schema-change dry-run complete this/
119
+ end
120
+
121
+
122
+ def test_announce_reverted
123
+ @test_migration.online_alter_table :foo_table, :execute do |t|
124
+ t.integer :new_column_name
125
+ end
126
+ @test_migration.announce 'reverted, this should get appended'
127
+ assert_not_nil @test_migration.puts_message =~ /pt-online-schema-change executed, reverted, this should get appended/
128
+ end
129
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pt-online-migration
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - LeadKarma, LLC
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-10-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: mysql2
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: mocha
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: active record migration wrapper for pt-online-schema-change cli command
95
+ email:
96
+ - support@leadkarma.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitattributes
102
+ - Gemfile
103
+ - LICENSE.txt
104
+ - README.md
105
+ - Rakefile
106
+ - lib/pt_online_migration.rb
107
+ - lib/pt_online_migration/pt_command_builder.rb
108
+ - lib/pt_online_migration/version.rb
109
+ - pt_online_migration.gemspec
110
+ - test/pt_online_migration_test.rb
111
+ homepage: http://www.leadkarma.com/
112
+ licenses:
113
+ - MIT
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ! '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 1.8.23
133
+ signing_key:
134
+ specification_version: 3
135
+ summary: online schema migrations for mysql
136
+ test_files:
137
+ - test/pt_online_migration_test.rb