pt-osc 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,6 @@ branches:
3
3
  - master
4
4
  language: ruby
5
5
  rvm:
6
- - 1.9.2
7
6
  - 1.9.3
8
7
  - 2.0.0
9
8
  - 2.1.2
@@ -1,3 +1,9 @@
1
+ ## 0.2.0
2
+
3
+ - support for setting the [`--check-alter` flag](http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html#cmdoption-pt-online-schema-change--%5Bno%5Dcheck-alter)
4
+ - internal improvements to the way `percona` options are handled
5
+ - removed support for Ruby 1.9.2
6
+
1
7
  ## 0.1.3
2
8
 
3
9
  - fix for loading percona config
data/README.md CHANGED
@@ -38,9 +38,22 @@ environment:
38
38
 
39
39
  Additional/modified options for the `percona` hash include:
40
40
  - `defaults-file`: Can be specified as an absolute path (with leading `/`) or relative (without). Relative paths will be treated as relative to your project's working directory.
41
+ - `check-alter`: When set to `false`, `ALTER` commands will not be checked when executing (same as [`--no-check-alter`](http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html#cmdoption-pt-online-schema-change--%5Bno%5Dcheck-alter))
41
42
  - `run_mode`: Specify `'execute'` to actually run `pt-online-schema-change` when the migration runs. Specify `'print'` to output the commands to run to STDOUT instead. Default is `'print'`.
42
43
  - `log`: Specify the file used for logging activity. Can be a relative or absolute path.
43
44
 
45
+ #### Migrations
46
+
47
+ To run migrations with `pt-online-schema-change`, you need to explicitly opt them in by changing their parent class to `ActiveRecord::PtOscMigration`. For instance, if your migration was
48
+ ```ruby
49
+ class CreateTeams < ActiveRecord::Migration
50
+ ```
51
+ you should change it to
52
+ ```ruby
53
+ class CreateTeams < ActiveRecord::PtOscMigration
54
+ ```
55
+ If you have migrations that you do not want to be run with `pt-online-schema-change`, leave the same parent class and they will be run normally.
56
+
44
57
  ## Caveats
45
58
 
46
59
  This gem is not considered production ready. There will be bugs.
@@ -49,7 +62,6 @@ This gem is not considered production ready. There will be bugs.
49
62
 
50
63
  `pt-osc` is tested against:
51
64
  - ActiveRecord 3.2 branch
52
- - Ruby 1.9.2
53
65
  - Ruby 1.9.3
54
66
  - Ruby 2.0.0
55
67
  - Ruby 2.1.2
@@ -57,6 +69,8 @@ This gem is not considered production ready. There will be bugs.
57
69
 
58
70
  Support for other versions of Ruby or ActiveRecord is unknown and not guaranteed.
59
71
 
72
+ `pt-osc` requires the use of `pt-online-schema-change` 2.0 or later. Some options may not work with all versions.
73
+
60
74
  `pt-osc` is compatible with versions 0.5.0 and later of [zdennis/activerecord-import](https://github.com/zdennis/activerecord-import). It will not work with earlier versions.
61
75
 
62
76
  #License and Copyright
@@ -9,10 +9,17 @@ module ActiveRecord
9
9
  mutator: :make_path_absolute,
10
10
  },
11
11
  'recursion-method' => {
12
+ version: '>= 2.1',
12
13
  },
13
14
  'execute' => {
14
15
  default: false,
15
16
  },
17
+ 'check-alter' => {
18
+ boolean: true,
19
+ default: true,
20
+ mutator: :execute_only,
21
+ version: '>= 2.1',
22
+ }
16
23
  }.freeze
17
24
 
18
25
  def self.percona_flags
@@ -140,18 +147,14 @@ module ActiveRecord
140
147
 
141
148
  # Set defaults
142
149
  self.class.percona_flags.each do |flag, flag_config|
143
- options[flag] ||= flag_config[:default] if flag_config.key?(:default)
150
+ options[flag] = flag_config[:default] if flag_config.key?(:default) && !options.key?(flag)
144
151
  end
145
152
 
146
- # Determine run mode
147
- command += options.delete(:execute) ? ' --execute' : ' --dry-run'
148
-
149
- options.each do |key, value|
150
- value = send(self.class.percona_flags[key][:mutator], value) if self.class.percona_flags[key].try(:key?, :mutator)
151
- command += " --#{key} #{value}"
152
- end
153
+ "#{command}#{run_mode_flag(options)}#{command_flags(options)}"
154
+ end
153
155
 
154
- command
156
+ def self.tool_version
157
+ @_tool_version ||= Gem::Version.new(get_tool_version.sub('pt-online-schema-change', '').strip)
155
158
  end
156
159
 
157
160
  def database_config
@@ -175,11 +178,48 @@ module ActiveRecord
175
178
  end
176
179
 
177
180
  private
181
+ def command_flags(options)
182
+ options.map do |key, value|
183
+ next if key == 'execute'
184
+ flag_options = self.class.percona_flags[key]
185
+
186
+ # Satisfy version requirements
187
+ if flag_options.try(:key?, :version)
188
+ next unless Gem::Requirement.new(flag_options[:version]).satisfied_by? self.class.tool_version
189
+ end
190
+
191
+ # Mutate the value if needed
192
+ if flag_options.try(:key?, :mutator)
193
+ value = send(flag_options[:mutator], value, all_options: options, flag_name: key)
194
+ end
195
+
196
+ # Handle boolean flags
197
+ if flag_options.try(:[], :boolean)
198
+ key = "no-#{key}" unless value
199
+ value = nil
200
+ end
201
+
202
+ " --#{key} #{value}"
203
+ end.join('')
204
+ end
205
+
206
+ def run_mode_flag(options)
207
+ options[:execute] ? ' --execute' : ' --dry-run'
208
+ end
209
+
210
+ def self.get_tool_version
211
+ `pt-online-schema-change --version`
212
+ end
213
+
178
214
  # Flag mutators
179
- def make_path_absolute(path)
215
+ def make_path_absolute(path, _ = {})
180
216
  return path if path[0] == '/'
181
217
  # If path is not already absolute, treat it as relative to the app root
182
218
  File.expand_path(path, Dir.getwd)
183
219
  end
220
+
221
+ def execute_only(flag, options = {})
222
+ options[:all_options][:execute] ? flag : self.class.percona_flags[options[:flag_name]][:default]
223
+ end
184
224
  end
185
225
  end
@@ -1,5 +1,5 @@
1
1
  module Pt
2
2
  module Osc
3
- VERSION = '0.1.3'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency 'rake'
22
22
  spec.add_development_dependency 'shoulda'
23
23
  spec.add_development_dependency 'faker'
24
- spec.add_development_dependency 'mocha'
24
+ spec.add_development_dependency 'mocha', '>= 0.9.0'
25
25
 
26
26
  # For testing using dummy Rails app
27
27
  spec.add_development_dependency 'rails', '~> 3.2'
@@ -24,3 +24,8 @@ test_execute_string:
24
24
  <<: *test
25
25
  percona:
26
26
  run_mode: 'execute'
27
+
28
+ test_no_check_alter:
29
+ <<: *test
30
+ percona:
31
+ check-alter: false
@@ -3,36 +3,45 @@ require 'test_helper'
3
3
  class PtOscMigrationFunctionalTest < ActiveRecord::TestCase
4
4
  class TestMigration < ActiveRecord::PtOscMigration; end
5
5
 
6
- context 'a migration connected to a pt-osc database' do
6
+ context 'a migration' do
7
7
  setup do
8
- ActiveRecord::Base.establish_connection(test_spec)
9
8
  @migration = TestMigration.new
10
- @migration.instance_variable_set(:@connection, ActiveRecord::Base.connection)
9
+ ActiveRecord::PtOscMigration.stubs(:tool_version).returns(Gem::Version.new('100'))
11
10
  end
12
11
 
13
- context 'on an existing table with an existing column' do
12
+ teardown do
13
+ ActiveRecord::PtOscMigration.unstub(:tool_version)
14
+ end
15
+
16
+ context 'connected to a pt-osc database' do
14
17
  setup do
15
- @table_name = Faker::Lorem.word
16
- @column_name = Faker::Lorem.word
17
- @index_name = Faker::Lorem.words.join('_')
18
- @index_name_2 = "#{@index_name}_2"
18
+ ActiveRecord::Base.establish_connection(test_spec)
19
+ @migration.instance_variable_set(:@connection, ActiveRecord::Base.connection)
20
+ end
21
+
22
+ context 'on an existing table with an existing column' do
23
+ setup do
24
+ @table_name = Faker::Lorem.word
25
+ @column_name = Faker::Lorem.word
26
+ @index_name = Faker::Lorem.words.join('_')
27
+ @index_name_2 = "#{@index_name}_2"
19
28
 
20
- ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS `#{@table_name}`;"
21
- ActiveRecord::Base.connection.execute <<-SQL
29
+ ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS `#{@table_name}`;"
30
+ ActiveRecord::Base.connection.execute <<-SQL
22
31
  CREATE TABLE `#{@table_name}` (
23
32
  `#{@column_name}` varchar(255) DEFAULT NULL,
24
33
  KEY `#{@index_name}` (`#{@column_name}`)
25
34
  );
26
- SQL
27
- end
35
+ SQL
36
+ end
28
37
 
29
- teardown do
30
- ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS `#{@table_name}`;"
31
- end
38
+ teardown do
39
+ ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS `#{@table_name}`;"
40
+ end
32
41
 
33
- context 'a migration with only ALTER statements' do
34
- setup do
35
- TestMigration.class_eval <<-EVAL
42
+ context 'a migration with only ALTER statements' do
43
+ setup do
44
+ TestMigration.class_eval <<-EVAL
36
45
  def change
37
46
  rename_table :#{@table_name}, :#{Faker::Lorem.word}
38
47
  add_column :#{@table_name}, :#{Faker::Lorem.word}, :integer
@@ -42,58 +51,79 @@ class PtOscMigrationFunctionalTest < ActiveRecord::TestCase
42
51
  add_index :#{@table_name}, :#{@column_name}, name: :#{@index_name_2}
43
52
  remove_index :#{@table_name}, name: :#{@index_name}
44
53
  end
45
- EVAL
46
- end
47
-
48
- teardown do
49
- TestMigration.send(:remove_method, :change)
50
- end
51
-
52
- context 'ignoring schema lookups' do
53
- setup do
54
- # Kind of a hacky way to do this
55
- ignored_sql = ActiveRecord::SQLCounter.ignored_sql + [
56
- /^SHOW FULL FIELDS FROM/,
57
- /^SHOW COLUMNS FROM/,
58
- /^SHOW KEYS FROM/,
59
- ]
60
- ActiveRecord::SQLCounter.any_instance.stubs(:ignore).returns(ignored_sql)
54
+ EVAL
61
55
  end
62
56
 
63
57
  teardown do
64
- ActiveRecord::SQLCounter.any_instance.unstub(:ignore)
58
+ TestMigration.send(:remove_method, :change)
65
59
  end
66
60
 
67
- context 'with suppressed output' do
61
+ context 'ignoring schema lookups' do
68
62
  setup do
69
- @migration.stubs(:write)
70
- @migration.stubs(:announce)
63
+ # Kind of a hacky way to do this
64
+ ignored_sql = ActiveRecord::SQLCounter.ignored_sql + [
65
+ /^SHOW FULL FIELDS FROM/,
66
+ /^SHOW COLUMNS FROM/,
67
+ /^SHOW KEYS FROM/,
68
+ ]
69
+ ActiveRecord::SQLCounter.any_instance.stubs(:ignore).returns(ignored_sql)
71
70
  end
72
71
 
73
72
  teardown do
74
- @migration.unstub(:write, :announce)
75
- end
76
-
77
- should 'not execute any queries immediately' do
78
- assert_no_queries { @migration.change }
73
+ ActiveRecord::SQLCounter.any_instance.unstub(:ignore)
79
74
  end
80
75
 
81
- context 'with a working pt-online-schema-change' do
76
+ context 'with suppressed output' do
82
77
  setup do
83
- Kernel.expects(:system).with(regexp_matches(/^pt-online-schema-change/)).twice.returns(true)
78
+ @migration.stubs(:write)
79
+ @migration.stubs(:announce)
84
80
  end
85
81
 
86
82
  teardown do
87
- Kernel.unstub(:system)
83
+ @migration.unstub(:write, :announce)
88
84
  end
89
85
 
90
- should 'not directly execute any queries when migrating' do
91
- assert_no_queries { @migration.migrate(:up) }
86
+ should 'not execute any queries immediately' do
87
+ assert_no_queries { @migration.change }
88
+ end
89
+
90
+ context 'with a working pt-online-schema-change' do
91
+ setup do
92
+ Kernel.expects(:system).with(regexp_matches(/^pt-online-schema-change/)).twice.returns(true)
93
+ end
94
+
95
+ teardown do
96
+ Kernel.unstub(:system)
97
+ end
98
+
99
+ should 'not directly execute any queries when migrating' do
100
+ assert_no_queries { @migration.migrate(:up) }
101
+ end
92
102
  end
93
103
  end
94
104
  end
95
105
  end
96
106
  end
97
107
  end
108
+
109
+ context 'connected without checking alter statements' do
110
+ setup do
111
+ @old_connection = @migration.instance_variable_get(:@connection)
112
+ ActiveRecord::Base.establish_connection(test_spec('test_no_check_alter'))
113
+ @migration.instance_variable_set(:@connection, ActiveRecord::Base.connection)
114
+ end
115
+
116
+ should 'check ALTER when dry-running sql' do
117
+ command = @migration.send(:percona_command, nil, nil, nil, execute: false)
118
+ assert command.include?('--check-alter'), "Command '#{command}' did not include ALTER check."
119
+ assert_equal false, command.include?('--no-check-alter'), "Command '#{command}' should not disable ALTER check."
120
+ end
121
+
122
+ should 'not check ALTER when executing sql' do
123
+ command = @migration.send(:percona_command, nil, nil, nil, execute: true)
124
+ assert command.include?('--no-check-alter'), "Command '#{command}' should not include ALTER check."
125
+ assert_equal false, command.include?('--check-alter'), "Command '#{command}' should disable ALTER check."
126
+ end
127
+ end
98
128
  end
99
129
  end
@@ -4,6 +4,12 @@ class PtOscMigrationUnitTest < Test::Unit::TestCase
4
4
  context 'with a pt-osc migration' do
5
5
  setup do
6
6
  @migration = ActiveRecord::PtOscMigration.new
7
+ @tool_version = states('tool_version').starts_as('100')
8
+ ActiveRecord::PtOscMigration.stubs(:tool_version).returns(Gem::Version.new('100')).when(@tool_version.is('100'))
9
+ end
10
+
11
+ teardown do
12
+ ActiveRecord::PtOscMigration.unstub(:tool_version)
7
13
  end
8
14
 
9
15
  context '#percona_command' do
@@ -47,7 +53,7 @@ class PtOscMigrationUnitTest < Test::Unit::TestCase
47
53
 
48
54
  should 'set missing flags to default values' do
49
55
  flags_with_defaults = ActiveRecord::PtOscMigration.percona_flags.select do |flag, config|
50
- config.key?(:default) && flag != 'execute'
56
+ config.key?(:default) && flag != 'execute' && !config[:boolean]
51
57
  end
52
58
 
53
59
  command = @migration.send(:percona_command, '', '', '')
@@ -58,6 +64,78 @@ class PtOscMigrationUnitTest < Test::Unit::TestCase
58
64
  end
59
65
  end
60
66
 
67
+ context 'with flags having version requirements' do
68
+ setup do
69
+ flags = ActiveRecord::PtOscMigration.percona_flags.merge({ 'flag-with-requirement' => { version: '> 2.0' } })
70
+ ActiveRecord::PtOscMigration.stubs(:percona_flags).returns(flags)
71
+ end
72
+
73
+ teardown do
74
+ ActiveRecord::PtOscMigration.unstub(:percona_flags)
75
+ end
76
+
77
+ context 'with matching tool version' do
78
+ setup do
79
+ @old_tool_version = @tool_version.current_state
80
+ ActiveRecord::PtOscMigration.stubs(:tool_version).returns(Gem::Version.new('2.6.40')).when(@tool_version.is('2.6.40'))
81
+ @tool_version.become('2.6.40')
82
+ end
83
+
84
+ teardown do
85
+ @tool_version.become(@old_tool_version)
86
+ end
87
+
88
+ should 'contain the flag in the output' do
89
+ command = @migration.send(:percona_command, '', '', '', 'flag-with-requirement' => 'foobar')
90
+ assert command.include?('--flag-with-requirement foobar'),
91
+ "Flag was not included in command '#{command}' despite meeting version requirement."
92
+ end
93
+ end
94
+
95
+ context 'with non-matching tool version' do
96
+ setup do
97
+ @old_tool_version = @tool_version.current_state
98
+ ActiveRecord::PtOscMigration.stubs(:tool_version).returns(Gem::Version.new('1.12')).when(@tool_version.is('1.12'))
99
+ @tool_version.become('1.12')
100
+ end
101
+
102
+ teardown do
103
+ @tool_version.become(@old_tool_version)
104
+ end
105
+
106
+ should 'not contain the flag in the output' do
107
+ command = @migration.send(:percona_command, '', '', '', 'flag-with-requirement' => 'foobar')
108
+ assert_equal false, command.include?('--flag-with-requirement'),
109
+ "Flag was included in command '#{command}' despite not meeting version requirement."
110
+ end
111
+ end
112
+ end
113
+
114
+ context 'with boolean flags' do
115
+ setup do
116
+ flags = ActiveRecord::PtOscMigration.percona_flags.merge({ 'boolean-flag' => { boolean: true } })
117
+ ActiveRecord::PtOscMigration.stubs(:percona_flags).returns(flags)
118
+ end
119
+
120
+ teardown do
121
+ ActiveRecord::PtOscMigration.unstub(:percona_flags)
122
+ end
123
+
124
+ should 'include just the flag (no value) when true' do
125
+ command = @migration.send(:percona_command, '', '', '', 'boolean-flag' => true)
126
+ matches_eof = command =~ /\-\-boolean\-flag\s*$/
127
+ matches_middle = command =~ /\-\-boolean\-flag\s*\-\-/
128
+ assert matches_eof || matches_middle, "Boolean flag was malformed in command '#{command}'."
129
+ end
130
+
131
+ should 'include a "no" version of the flag when false' do
132
+ command = @migration.send(:percona_command, '', '', '', 'boolean-flag' => false)
133
+ matches_eof = command =~ /\-\-no\-boolean\-flag\s*$/
134
+ matches_middle = command =~ /\-\-no\-boolean\-flag\s*\-\-/
135
+ assert matches_eof || matches_middle, "Boolean flag was malformed in command '#{command}'."
136
+ end
137
+ end
138
+
61
139
  should 'perform a dry run if execute not specified' do
62
140
  command = @migration.send(:percona_command, '', '', '')
63
141
  assert command.include?('--dry-run')
@@ -76,7 +154,7 @@ class PtOscMigrationUnitTest < Test::Unit::TestCase
76
154
  end
77
155
 
78
156
  should 'call #make_path_absolute' do
79
- @migration.expects(:make_path_absolute).with(@path)
157
+ @migration.expects(:make_path_absolute).with(@path, anything)
80
158
  @migration.send(:percona_command, '', '', '', @options)
81
159
  end
82
160
  end
@@ -193,6 +271,24 @@ class PtOscMigrationUnitTest < Test::Unit::TestCase
193
271
  end
194
272
  end
195
273
 
274
+ context '#execute_only' do
275
+ should 'return the default flag value during a dry run' do
276
+ ActiveRecord::PtOscMigration.stubs(:percona_flags).returns('foo' => { default: 'bar' })
277
+ assert_equal 'bar', @migration.send(:execute_only, 'baz', all_options: { execute: false }, flag_name: 'foo')
278
+ ActiveRecord::PtOscMigration.unstub(:percona_flags)
279
+ end
280
+
281
+ should 'return nil during a dry run if there is no default value' do
282
+ ActiveRecord::PtOscMigration.stubs(:percona_flags).returns('foo' => {})
283
+ assert_nil @migration.send(:execute_only, 'baz', all_options: { execute: false }, flag_name: 'foo')
284
+ ActiveRecord::PtOscMigration.unstub(:percona_flags)
285
+ end
286
+
287
+ should 'pass the flag through when executing' do
288
+ assert_equal 'baz', @migration.send(:execute_only, 'baz', all_options: { execute: true }, flag_name: 'foo')
289
+ end
290
+ end
291
+
196
292
  context '#execute_pt_osc' do
197
293
  context 'with a pt-osc connection' do
198
294
  setup do
@@ -425,4 +521,21 @@ class PtOscMigrationUnitTest < Test::Unit::TestCase
425
521
  end
426
522
  end
427
523
  end
524
+
525
+ context '#tool_version' do
526
+ context 'with known tool version' do
527
+ setup do
528
+ ActiveRecord::PtOscMigration.stubs(:get_tool_version).returns('pt-online-schema-change 2.2.7')
529
+ end
530
+
531
+ teardown do
532
+ ActiveRecord::PtOscMigration.unstub(:get_tool_version)
533
+ end
534
+
535
+ should 'return a Gem::Version with the expected value' do
536
+ assert_instance_of Gem::Version, ActiveRecord::PtOscMigration.send(:tool_version)
537
+ assert_equal '2.2.7', ActiveRecord::PtOscMigration.send(:tool_version).version
538
+ end
539
+ end
540
+ end
428
541
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pt-osc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-10-05 00:00:00.000000000 Z
12
+ date: 2015-01-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -82,7 +82,7 @@ dependencies:
82
82
  requirements:
83
83
  - - ! '>='
84
84
  - !ruby/object:Gem::Version
85
- version: '0'
85
+ version: 0.9.0
86
86
  type: :development
87
87
  prerelease: false
88
88
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  requirements:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
- version: '0'
93
+ version: 0.9.0
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: rails
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -246,7 +246,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
246
246
  version: '0'
247
247
  segments:
248
248
  - 0
249
- hash: -738209304282433145
249
+ hash: 2714793465659180068
250
250
  required_rubygems_version: !ruby/object:Gem::Requirement
251
251
  none: false
252
252
  requirements:
@@ -255,7 +255,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
255
255
  version: '0'
256
256
  segments:
257
257
  - 0
258
- hash: -738209304282433145
258
+ hash: 2714793465659180068
259
259
  requirements: []
260
260
  rubyforge_project:
261
261
  rubygems_version: 1.8.23