lhm 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,16 +1,10 @@
1
+ language: ruby
1
2
  before_script:
2
3
  - "mysql -e 'create database lhm;'"
3
4
  rvm:
4
5
  - 1.8.7
5
6
  - 1.9.3
6
7
  gemfile:
7
- - gemfiles/ar-2.3.gemfile
8
- - gemfiles/ar-3.1.gemfile
9
- matrix:
10
- exclude:
11
- - rvm: 1.8.7
12
- gemfile: gemfiles/ar-2.3.gemfile
13
- branches:
14
- only:
15
- - master
16
- - stable
8
+ - gemfiles/ar-2.3_mysql.gemfile
9
+ - gemfiles/ar-3.2_mysql.gemfile
10
+ - gemfiles/ar-3.2_mysql2.gemfile
@@ -1,3 +1,9 @@
1
+ # 1.1.0 (April 29, 2012)
2
+
3
+ * Add option to specify custom index name
4
+ * Add mysql2 compatibility
5
+ * Add AtomicSwitcher
6
+
1
7
  # 1.0.3 (February 23, 2012)
2
8
 
3
9
  * Improve change_column
data/README.md CHANGED
@@ -1,6 +1,4 @@
1
- # Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png)](http://travis-ci.org/soundcloud/large-hadron-migrator)
2
-
3
- Update: There is currently [An issue](https://github.com/soundcloud/large-hadron-migrator/issues/11) with the migration. Fix coming up.
1
+ # Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png)][4]
4
2
 
5
3
  Rails style database migrations are a useful way to evolve your data schema in
6
4
  an agile manner. Most Rails projects start like this, and at first, making
@@ -22,7 +20,7 @@ table. The InnoDB Plugin provides facilities for online index creation, which
22
20
  is great if you are using this engine, but only solves half the problem.
23
21
 
24
22
  At SoundCloud we started having migration pains quite a while ago, and after
25
- looking around for third party solutions [0] [1] [2], we decided to create our
23
+ looking around for third party solutions, we decided to create our
26
24
  own. We called it Large Hadron Migrator, and it is a gem for online
27
25
  ActiveRecord migrations.
28
26
 
@@ -33,62 +31,100 @@ ActiveRecord migrations.
33
31
  ## The idea
34
32
 
35
33
  The basic idea is to perform the migration online while the system is live,
36
- without locking the table. Similar to OAK (online alter table) [2] and the
37
- facebook tool [0], we use a copy table, triggers and a journal table.
34
+ without locking the table. In contrast to [OAK][0] and the
35
+ [facebook tool][1], we only use a copy table and triggers.
38
36
 
39
37
  The Large Hadron is a test driven Ruby solution which can easily be dropped
40
38
  into an ActiveRecord migration. It presumes a single auto incremented
41
39
  numerical primary key called id as per the Rails convention. Unlike the
42
- twitter solution [1], it does not require the presence of an indexed
40
+ [twitter solution][2], it does not require the presence of an indexed
43
41
  `updated_at` column.
44
42
 
43
+ ## Requirements
44
+
45
+ Lhm currently only works with MySQL databases and requires an established
46
+ ActiveRecord connection.
47
+
48
+ It is compatible and [continuously tested][4] with Ruby 1.8.7 and Ruby 1.9.x,
49
+ ActiveRecord 2.3.x and 3.x as well as mysql and mysql2 adapters.
50
+
51
+ ## Installation
52
+
53
+ Install it via `gem install lhm` or add `gem "lhm"` to your Gemfile.
54
+
45
55
  ## Usage
46
56
 
47
- You can invoke Lhm directly from a plain ruby file after connecting active
48
- record to your mysql instance:
57
+ You can invoke Lhm directly from a plain ruby file after connecting ActiveRecord
58
+ to your mysql instance:
49
59
 
50
- require 'lhm'
60
+ ```ruby
61
+ require 'lhm'
51
62
 
52
- ActiveRecord::Base.establish_connection(
53
- :adapter => 'mysql',
54
- :host => '127.0.0.1',
55
- :database => 'lhm'
56
- )
63
+ ActiveRecord::Base.establish_connection(
64
+ :adapter => 'mysql',
65
+ :host => '127.0.0.1',
66
+ :database => 'lhm'
67
+ )
57
68
 
58
- Lhm.change_table(:users) do |m|
59
- m.add_column(:arbitrary, "INT(12)")
60
- m.add_index([:arbitrary, :created_at])
61
- m.ddl("alter table %s add column flag tinyint(1)" % m.name)
62
- end
69
+ Lhm.change_table :users do |m|
70
+ m.add_column :arbitrary, "INT(12)"
71
+ m.add_index [:arbitrary_id, :created_at]
72
+ m.ddl("alter table %s add column flag tinyint(1)" % m.name)
73
+ end
74
+ ```
63
75
 
64
76
  To use Lhm from an ActiveRecord::Migration in a Rails project, add it to your
65
77
  Gemfile, then invoke as follows:
66
78
 
67
- class MigrateUsers < ActiveRecord::Migration
68
-
69
- def self.up
70
- Lhm.change_table(:users) do |m|
71
- m.add_column(:arbitrary, "INT(12)")
72
- m.add_index([:arbitrary, :created_at])
73
- m.ddl("alter table %s add column flag tinyint(1)" % m.name)
74
- end
75
- end
76
-
77
- def self.down
78
- Lhm.change_table(:users) do |m|
79
- m.remove_index([:arbitrary, :created_at])
80
- m.remove_column(:arbitrary)
81
- end
82
- end
79
+ ```ruby
80
+ require 'lhm'
81
+
82
+ class MigrateUsers < ActiveRecord::Migration
83
+ def self.up
84
+ Lhm.change_table :users do |m|
85
+ m.add_column :arbitrary, "INT(12)"
86
+ m.add_index [:arbitrary_id, :created_at]
87
+ m.ddl("alter table %s add column flag tinyint(1)" % m.name)
88
+ end
89
+ end
90
+
91
+ def self.down
92
+ Lhm.change_table :users do |m|
93
+ m.remove_index [:arbitrary_id, :created_at]
94
+ m.remove_column :arbitrary)
83
95
  end
96
+ end
97
+ end
98
+ ```
99
+
100
+ ## Table rename strategies
101
+
102
+ There are two different table rename strategies available: LockedSwitcher and
103
+ AtomicSwitcher.
104
+
105
+ For all setups which use replication and a MySQL version
106
+ affected by the the [binlog bug #39675](http://bugs.mysql.com/bug.php?id=39675),
107
+ we recommend the LockedSwitcher strategy to avoid replication issues. This
108
+ strategy locks the table being migrated and issues two ALTER TABLE statements.
109
+ The AtomicSwitcher uses a single atomic RENAME TABLE query and should be favored
110
+ in setups which do not suffer from the mentioned replication bug.
111
+
112
+ Lhm chooses the strategy automatically based on the used MySQL server version,
113
+ but you can override the behavior with an option:
114
+
115
+ ```ruby
116
+ Lhm.change_table :users, :atomic_switch => true do |m|
117
+ # ...
118
+ end
119
+ ```
84
120
 
85
121
  ## Contributing
86
122
 
87
123
  We'll check out your contribution if you:
88
124
 
89
- - Provide a comprehensive suite of tests for your fork.
90
- - Have a clear and documented rationale for your changes.
91
- - Package these up in a pull request.
125
+ * Provide a comprehensive suite of tests for your fork.
126
+ * Have a clear and documented rationale for your changes.
127
+ * Package these up in a pull request.
92
128
 
93
129
  We'll do our best to help you out with any contribution issues you may have.
94
130
 
@@ -96,9 +132,15 @@ We'll do our best to help you out with any contribution issues you may have.
96
132
 
97
133
  The license is included as LICENSE in this directory.
98
134
 
99
- ## Footnotes
135
+ ## Similar solutions
100
136
 
101
- [0]: http://www.facebook.com/note.php?note\_id=430801045932 "Facebook"
102
- [1]: https://github.com/freels/table\_migrator "Twitter"
103
- [2]: http://openarkkit.googlecode.com "OAK online alter table"
137
+ * [OAK: online alter table][0]
138
+ * [Facebook][1]
139
+ * [Twitter][2]
140
+ * [pt-online-schema-change][3]
104
141
 
142
+ [0]: http://openarkkit.googlecode.com
143
+ [1]: http://www.facebook.com/note.php?note\_id=430801045932
144
+ [2]: https://github.com/freels/table_migrator
145
+ [3]: http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html
146
+ [4]: http://travis-ci.org/soundcloud/large-hadron-migrator
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ for gemfile in gemfiles/*.gemfile
4
+ do
5
+ if !(BUNDLE_GEMFILE=$gemfile bundle install &&
6
+ BUNDLE_GEMFILE=$gemfile bundle exec rake)
7
+ then
8
+ exit 1
9
+ fi
10
+ done
@@ -1,4 +1,5 @@
1
- source "http://rubygems.org"
1
+ source :rubygems
2
2
 
3
+ gem "mysql", "~> 2.8.1"
3
4
  gem "activerecord", "~> 2.3.14"
4
5
  gemspec :path=>"../"
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gem "mysql", "~> 2.8.1"
4
+ gem "activerecord", "~> 3.2.2"
5
+ gemspec :path=>"../"
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gem "mysql2", "~> 0.3.11"
4
+ gem "activerecord", "~> 3.2.2"
5
+ gemspec :path=>"../"
@@ -19,8 +19,6 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
  s.executables = ["lhm-kill-queue"]
21
21
 
22
- # this should be a real dependency, but we're using a different gem in our code
23
- s.add_development_dependency "mysql", "~> 2.8.1"
24
22
  s.add_development_dependency "minitest", "= 2.10.0"
25
23
  s.add_development_dependency "rake"
26
24
 
data/lib/lhm.rb CHANGED
@@ -21,22 +21,25 @@ module Lhm
21
21
  # Alters a table with the changes described in the block
22
22
  #
23
23
  # @param [String, Symbol] table_name Name of the table
24
- # @param [Hash] chunk_options Optional options to alter the chunk behavior
25
- # @option chunk_options [Fixnum] :stride
24
+ # @param [Hash] options Optional options to alter the chunk / switch behavior
25
+ # @option options [Fixnum] :stride
26
26
  # Size of a chunk (defaults to: 40,000)
27
- # @option chunk_options [Fixnum] :throttle
27
+ # @option options [Fixnum] :throttle
28
28
  # Time to wait between chunks in milliseconds (defaults to: 100)
29
+ # @option options [Boolean] :atomic_switch
30
+ # Use atomic switch to rename tables (defaults to: true)
31
+ # If using a version of mysql affected by atomic switch bug, LHM forces user
32
+ # to set this option (see SqlHelper#supports_atomic_switch?)
29
33
  # @yield [Migrator] Yielded Migrator object records the changes
30
34
  # @return [Boolean] Returns true if the migration finishes
31
35
  # @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
32
- def self.change_table(table_name, chunk_options = {}, &block)
36
+ def self.change_table(table_name, options = {}, &block)
33
37
  connection = ActiveRecord::Base.connection
34
38
  origin = Table.parse(table_name, connection)
35
39
  invoker = Invoker.new(origin, connection)
36
40
  block.call(invoker.migrator)
37
- invoker.run(chunk_options)
41
+ invoker.run(options)
38
42
 
39
43
  true
40
44
  end
41
45
  end
42
-
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require 'lhm/command'
5
+ require 'lhm/migration'
6
+ require 'lhm/sql_helper'
7
+
8
+ module Lhm
9
+ # Switches origin with destination table using an atomic rename.
10
+ #
11
+ # It should only be used if the MySQL server version is not affected by the
12
+ # bin log affecting bug #39675. This can be verified using
13
+ # Lhm::SqlHelper.supports_atomic_switch?.
14
+ class AtomicSwitcher
15
+ include Command
16
+ include SqlHelper
17
+
18
+ attr_reader :connection
19
+
20
+ def initialize(migration, connection = nil)
21
+ @migration = migration
22
+ @connection = connection
23
+ @origin = migration.origin
24
+ @destination = migration.destination
25
+ end
26
+
27
+ def statements
28
+ atomic_switch
29
+ end
30
+
31
+ def atomic_switch
32
+ [
33
+ "rename table `#{ @origin.name }` to `#{ @migration.archive_name }`, " +
34
+ "`#{ @destination.name }` to `#{ @origin.name }`"
35
+ ]
36
+ end
37
+
38
+ def validate
39
+ unless table?(@origin.name) && table?(@destination.name)
40
+ error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
41
+ end
42
+ end
43
+
44
+ private
45
+ def execute
46
+ sql statements
47
+ end
48
+ end
49
+ end
@@ -91,6 +91,7 @@ module Lhm
91
91
 
92
92
  print "."
93
93
  end
94
+ print "\n"
94
95
  end
95
96
  end
96
97
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'lhm/chunker'
5
5
  require 'lhm/entangler'
6
+ require 'lhm/atomic_switcher'
6
7
  require 'lhm/locked_switcher'
7
8
  require 'lhm/migrator'
8
9
 
@@ -13,19 +14,35 @@ module Lhm
13
14
  # Once the origin and destination tables have converged, origin is archived
14
15
  # and replaced by destination.
15
16
  class Invoker
16
- attr_reader :migrator
17
+ include SqlHelper
18
+
19
+ attr_reader :migrator, :connection
17
20
 
18
21
  def initialize(origin, connection)
19
22
  @connection = connection
20
23
  @migrator = Migrator.new(origin, connection)
21
24
  end
22
25
 
23
- def run(chunk_options = {})
26
+ def run(options = {})
27
+ if !options.include?(:atomic_switch)
28
+ if supports_atomic_switch?
29
+ options[:atomic_switch] = true
30
+ else
31
+ raise Error.new(
32
+ "Using mysql #{version_string}. You must explicitly set " +
33
+ "options[:atomic_switch] (re SqlHelper#supports_atomic_switch?)")
34
+ end
35
+ end
36
+
24
37
  migration = @migrator.run
25
38
 
26
39
  Entangler.new(migration, @connection).run do
27
- Chunker.new(migration, @connection, chunk_options).run
28
- LockedSwitcher.new(migration, @connection).run
40
+ Chunker.new(migration, @connection, options).run
41
+ if options[:atomic_switch]
42
+ AtomicSwitcher.new(migration, @connection).run
43
+ else
44
+ LockedSwitcher.new(migration, @connection).run
45
+ end
29
46
  end
30
47
  end
31
48
  end
@@ -6,11 +6,7 @@ require 'lhm/migration'
6
6
  require 'lhm/sql_helper'
7
7
 
8
8
  module Lhm
9
- # Switches origin with destination table with a write lock. Use this as
10
- # a safe alternative to rename, which can cause slave inconsistencies:
11
- #
12
- # http://bugs.mysql.com/bug.php?id=39675
13
- #
9
+ # Switches origin with destination table nonatomically using a locked write.
14
10
  # LockedSwitcher adopts the Facebook strategy, with the following caveat:
15
11
  #
16
12
  # "Since alter table causes an implicit commit in innodb, innodb locks get
@@ -95,8 +95,10 @@ module Lhm
95
95
  # @param [String, Symbol, Array<String, Symbol>] columns
96
96
  # A column name given as String or Symbol. An Array of Strings or Symbols
97
97
  # for compound indexes. It's possible to pass a length limit.
98
- def add_index(columns)
99
- ddl(index_ddl(columns))
98
+ # @param [String, Symbol] index_name
99
+ # Optional name of the index to be created
100
+ def add_index(columns, index_name = nil)
101
+ ddl(index_ddl(columns, false, index_name))
100
102
  end
101
103
 
102
104
  # Add a unique index to a table
@@ -112,8 +114,10 @@ module Lhm
112
114
  # @param [String, Symbol, Array<String, Symbol>] columns
113
115
  # A column name given as String or Symbol. An Array of Strings or Symbols
114
116
  # for compound indexes. It's possible to pass a length limit.
115
- def add_unique_index(columns)
116
- ddl(index_ddl(columns, :unique))
117
+ # @param [String, Symbol] index_name
118
+ # Optional name of the index to be created
119
+ def add_unique_index(columns, index_name = nil)
120
+ ddl(index_ddl(columns, true, index_name))
117
121
  end
118
122
 
119
123
  # Remove an index from a table
@@ -128,8 +132,11 @@ module Lhm
128
132
  # @param [String, Symbol, Array<String, Symbol>] columns
129
133
  # A column name given as String or Symbol. An Array of Strings or Symbols
130
134
  # for compound indexes.
131
- def remove_index(columns)
132
- ddl("drop index `%s` on `%s`" % [idx_name(@origin.name, columns), @name])
135
+ # @param [String, Symbol] index_name
136
+ # Optional name of the index to be removed
137
+ def remove_index(columns, index_name = nil)
138
+ index_name ||= idx_name(@origin.name, columns)
139
+ ddl("drop index `%s` on `%s`" % [index_name, @name])
133
140
  end
134
141
 
135
142
  private
@@ -167,9 +174,10 @@ module Lhm
167
174
  Table.parse(@origin.destination_name, connection)
168
175
  end
169
176
 
170
- def index_ddl(cols, unique = nil)
177
+ def index_ddl(cols, unique = nil, index_name = nil)
171
178
  type = unique ? "unique index" : "index"
172
- parts = [type, idx_name(@origin.name, cols), @name, idx_spec(cols)]
179
+ index_name ||= idx_name(@origin.name, cols)
180
+ parts = [type, index_name, @name, idx_spec(cols)]
173
181
  "create %s `%s` on `%s` (%s)" % parts
174
182
  end
175
183
  end
@@ -28,7 +28,7 @@ module Lhm
28
28
  [statements].flatten.each do |statement|
29
29
  connection.execute(tagged(statement))
30
30
  end
31
- rescue ActiveRecord::StatementInvalid, Mysql::Error => e
31
+ rescue ActiveRecord::StatementInvalid => e
32
32
  error e.message
33
33
  end
34
34
 
@@ -36,10 +36,14 @@ module Lhm
36
36
  [statements].flatten.inject(0) do |memo, statement|
37
37
  memo += connection.update(tagged(statement))
38
38
  end
39
- rescue ActiveRecord::StatementInvalid, Mysql::Error => e
39
+ rescue ActiveRecord::StatementInvalid => e
40
40
  error e.message
41
41
  end
42
42
 
43
+ def version_string
44
+ connection.select_one("show variables like 'version'")["Value"]
45
+ end
46
+
43
47
  private
44
48
 
45
49
  def tagged(statement)
@@ -51,5 +55,31 @@ module Lhm
51
55
  column.to_s.match(/`?([^\(]+)`?(\([^\)]+\))?/).captures
52
56
  end
53
57
  end
58
+
59
+ # Older versions of MySQL contain an atomic rename bug affecting bin
60
+ # log order. Affected versions extracted from bug report:
61
+ #
62
+ # http://bugs.mysql.com/bug.php?id=39675
63
+ #
64
+ # More Info: http://dev.mysql.com/doc/refman/5.5/en/metadata-locking.html
65
+ def supports_atomic_switch?
66
+ major, minor, tiny = version_string.split('.').map(&:to_i)
67
+
68
+ case major
69
+ when 4 then return false if minor and minor < 2
70
+ when 5
71
+ case minor
72
+ when 0 then return false if tiny and tiny < 52
73
+ when 1 then return false
74
+ when 4 then return false if tiny and tiny < 4
75
+ when 5 then return false if tiny and tiny < 3
76
+ end
77
+ when 6
78
+ case minor
79
+ when 0 then return false if tiny and tiny < 11
80
+ end
81
+ end
82
+ return true
83
+ end
54
84
  end
55
85
  end
@@ -38,7 +38,9 @@ module Lhm
38
38
 
39
39
  def ddl
40
40
  sql = "show create table `#{ @table_name }`"
41
- @connection.execute(sql).fetch_row.last
41
+ specification = nil
42
+ @connection.execute(sql).each { |row| specification = row.last }
43
+ specification
42
44
  end
43
45
 
44
46
  def parse
@@ -2,5 +2,5 @@
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
5
- VERSION = "1.0.3"
5
+ VERSION = "1.1.0"
6
6
  end
@@ -0,0 +1,42 @@
1
+ # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
5
+
6
+ require 'lhm/table'
7
+ require 'lhm/migration'
8
+ require 'lhm/atomic_switcher'
9
+
10
+ describe Lhm::AtomicSwitcher do
11
+ include IntegrationHelper
12
+
13
+ before(:each) { connect_master! }
14
+
15
+ describe "switching" do
16
+ before(:each) do
17
+ @origin = table_create("origin")
18
+ @destination = table_create("destination")
19
+ @migration = Lhm::Migration.new(@origin, @destination)
20
+ end
21
+
22
+ it "rename origin to archive" do
23
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
24
+ switcher.run
25
+
26
+ slave do
27
+ table_exists?(@origin).must_equal true
28
+ table_read(@migration.archive_name).columns.keys.must_include "origin"
29
+ end
30
+ end
31
+
32
+ it "rename destination to origin" do
33
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
34
+ switcher.run
35
+
36
+ slave do
37
+ table_exists?(@destination).must_equal false
38
+ table_read(@origin.name).columns.keys.must_include "destination"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -4,34 +4,39 @@
4
4
  require File.expand_path(File.dirname(__FILE__)) + "/../bootstrap"
5
5
 
6
6
  require 'active_record'
7
+ begin
8
+ require 'mysql2'
9
+ rescue LoadError
10
+ require 'mysql'
11
+ end
7
12
  require 'lhm/table'
8
13
  require 'lhm/sql_helper'
9
14
 
10
15
  module IntegrationHelper
11
- attr_accessor :connection
12
-
13
16
  #
14
17
  # Connectivity
15
18
  #
16
19
 
20
+ def connection
21
+ ActiveRecord::Base.connection
22
+ end
23
+
17
24
  def connect_master!
18
- @connection = connect!(3306)
25
+ connect!(3306)
19
26
  end
20
27
 
21
28
  def connect_slave!
22
- @connection = connect!(3307)
29
+ connect!(3307)
23
30
  end
24
31
 
25
32
  def connect!(port)
26
33
  ActiveRecord::Base.establish_connection(
27
- :adapter => 'mysql',
34
+ :adapter => defined?(Mysql2) ? 'mysql2' : 'mysql',
28
35
  :host => '127.0.0.1',
29
36
  :database => 'lhm',
30
37
  :username => '',
31
38
  :port => port
32
39
  )
33
-
34
- ActiveRecord::Base.connection
35
40
  end
36
41
 
37
42
  def select_one(*args)
@@ -43,7 +48,17 @@ module IntegrationHelper
43
48
  end
44
49
 
45
50
  def execute(*args)
46
- connection.execute(*args)
51
+ retries = 10
52
+ begin
53
+ connection.execute(*args)
54
+ rescue ActiveRecord::StatementInvalid => e
55
+ if (retries -= 1) > 0 && e.message =~ /Table '.*?' doesn't exist/
56
+ sleep 0.1
57
+ retry
58
+ else
59
+ raise
60
+ end
61
+ end
47
62
  end
48
63
 
49
64
  def slave(&block)
@@ -99,11 +114,16 @@ module IntegrationHelper
99
114
  select_value(query).to_i
100
115
  end
101
116
 
102
- def key?(table_name, cols, type = :non_unique)
103
- non_unique = type == :non_unique ? 1 : 0
117
+ def index_on_columns?(table_name, cols, type = :non_unique)
104
118
  key_name = Lhm::SqlHelper.idx_name(table_name, cols)
105
119
 
106
- !!select_value(%Q<
120
+ index?(table_name, key_name, type)
121
+ end
122
+
123
+ def index?(table_name, key_name, type = :non_unique)
124
+ non_unique = type == :non_unique ? 1 : 0
125
+
126
+ !!select_one(%Q<
107
127
  show indexes in #{ table_name }
108
128
  where key_name = '#{ key_name }'
109
129
  and non_unique = #{ non_unique }
@@ -16,7 +16,7 @@ describe Lhm do
16
16
  end
17
17
 
18
18
  it "should add a column" do
19
- Lhm.change_table(:users) do |t|
19
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
20
20
  t.add_column(:logins, "INT(12) DEFAULT '0'")
21
21
  end
22
22
 
@@ -32,7 +32,7 @@ describe Lhm do
32
32
  it "should copy all rows" do
33
33
  23.times { |n| execute("insert into users set reference = '#{ n }'") }
34
34
 
35
- Lhm.change_table(:users) do |t|
35
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
36
36
  t.add_column(:logins, "INT(12) DEFAULT '0'")
37
37
  end
38
38
 
@@ -42,7 +42,7 @@ describe Lhm do
42
42
  end
43
43
 
44
44
  it "should remove a column" do
45
- Lhm.change_table(:users) do |t|
45
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
46
46
  t.remove_column(:comment)
47
47
  end
48
48
 
@@ -52,47 +52,67 @@ describe Lhm do
52
52
  end
53
53
 
54
54
  it "should add an index" do
55
- Lhm.change_table(:users) do |t|
55
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
56
56
  t.add_index([:comment, :created_at])
57
57
  end
58
58
 
59
59
  slave do
60
- key?(:users, [:comment, :created_at]).must_equal(true)
60
+ index_on_columns?(:users, [:comment, :created_at]).must_equal(true)
61
+ end
62
+ end
63
+
64
+ it "should add an index with a custom name" do
65
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
66
+ t.add_index([:comment, :created_at], :my_index_name)
67
+ end
68
+
69
+ slave do
70
+ index?(:users, :my_index_name).must_equal(true)
61
71
  end
62
72
  end
63
73
 
64
74
  it "should add an index on a column with a reserved name" do
65
- Lhm.change_table(:users) do |t|
75
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
66
76
  t.add_index(:group)
67
77
  end
68
78
 
69
79
  slave do
70
- key?(:users, :group).must_equal(true)
80
+ index_on_columns?(:users, :group).must_equal(true)
71
81
  end
72
82
  end
73
83
 
74
84
  it "should add a unqiue index" do
75
- Lhm.change_table(:users) do |t|
85
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
76
86
  t.add_unique_index(:comment)
77
87
  end
78
88
 
79
89
  slave do
80
- key?(:users, :comment, :unique).must_equal(true)
90
+ index_on_columns?(:users, :comment, :unique).must_equal(true)
81
91
  end
82
92
  end
83
93
 
84
94
  it "should remove an index" do
85
- Lhm.change_table(:users) do |t|
95
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
86
96
  t.remove_index([:username, :created_at])
87
97
  end
88
98
 
89
99
  slave do
90
- key?(:users, [:username, :created_at]).must_equal(false)
100
+ index_on_columns?(:users, [:username, :created_at]).must_equal(false)
101
+ end
102
+ end
103
+
104
+ it "should remove an index with a custom name" do
105
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
106
+ t.remove_index(:reference, :index_users_on_reference)
107
+ end
108
+
109
+ slave do
110
+ index?(:users, :index_users_on_reference).must_equal(false)
91
111
  end
92
112
  end
93
113
 
94
114
  it "should apply a ddl statement" do
95
- Lhm.change_table(:users) do |t|
115
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
96
116
  t.ddl("alter table %s add column flag tinyint(1)" % t.name)
97
117
  end
98
118
 
@@ -106,7 +126,7 @@ describe Lhm do
106
126
  end
107
127
 
108
128
  it "should change a column" do
109
- Lhm.change_table(:users) do |t|
129
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
110
130
  t.change_column(:comment, "varchar(20) DEFAULT 'none' NOT NULL")
111
131
  end
112
132
 
@@ -122,7 +142,7 @@ describe Lhm do
122
142
  it "should change the last column in a table" do
123
143
  table_create(:small_table)
124
144
 
125
- Lhm.change_table(:small_table) do |t|
145
+ Lhm.change_table(:small_table, :atomic_switch => false) do |t|
126
146
  t.change_column(:id, "int(5)")
127
147
  end
128
148
 
@@ -146,7 +166,8 @@ describe Lhm do
146
166
  end
147
167
  end
148
168
 
149
- Lhm.change_table(:users, :stride => 10, :throttle => 97) do |t|
169
+ options = { :stride => 10, :throttle => 97, :atomic_switch => false }
170
+ Lhm.change_table(:users, options) do |t|
150
171
  t.add_column(:parallel, "INT(10) DEFAULT '0'")
151
172
  end
152
173
 
@@ -167,7 +188,8 @@ describe Lhm do
167
188
  end
168
189
  end
169
190
 
170
- Lhm.change_table(:users, :stride => 10, :throttle => 97) do |t|
191
+ options = { :stride => 10, :throttle => 97, :atomic_switch => false }
192
+ Lhm.change_table(:users, options) do |t|
171
193
  t.add_column(:parallel, "INT(10) DEFAULT '0'")
172
194
  end
173
195
 
@@ -0,0 +1,31 @@
1
+ # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
5
+
6
+ require 'lhm/table'
7
+ require 'lhm/migration'
8
+ require 'lhm/atomic_switcher'
9
+
10
+ describe Lhm::AtomicSwitcher do
11
+ include UnitHelper
12
+
13
+ before(:each) do
14
+ @start = Time.now
15
+ @origin = Lhm::Table.new("origin")
16
+ @destination = Lhm::Table.new("destination")
17
+ @migration = Lhm::Migration.new(@origin, @destination, @start)
18
+ @switcher = Lhm::AtomicSwitcher.new(@migration, nil)
19
+ end
20
+
21
+ describe "atomic switch" do
22
+ it "should perform a single atomic rename" do
23
+ @switcher.
24
+ statements.
25
+ must_equal([
26
+ "rename table `origin` to `#{ @migration.archive_name }`, " +
27
+ "`destination` to `origin`"
28
+ ])
29
+ end
30
+ end
31
+ end
@@ -15,7 +15,7 @@ describe Lhm::LockedSwitcher do
15
15
  @origin = Lhm::Table.new("origin")
16
16
  @destination = Lhm::Table.new("destination")
17
17
  @migration = Lhm::Migration.new(@origin, @destination, @start)
18
- @switcher = Lhm::LockedSwitcher.new(@migration)
18
+ @switcher = Lhm::LockedSwitcher.new(@migration, nil)
19
19
  end
20
20
 
21
21
  describe "uncommitted" do
@@ -16,14 +16,22 @@ describe Lhm::Migrator do
16
16
 
17
17
  describe "index changes" do
18
18
  it "should add an index" do
19
- @creator.add_index(["a", "b"])
19
+ @creator.add_index(:a)
20
+
21
+ @creator.statements.must_equal([
22
+ "create index `index_alt_on_a` on `lhmn_alt` (`a`)"
23
+ ])
24
+ end
25
+
26
+ it "should add a composite index" do
27
+ @creator.add_index([:a, :b])
20
28
 
21
29
  @creator.statements.must_equal([
22
30
  "create index `index_alt_on_a_and_b` on `lhmn_alt` (`a`, `b`)"
23
31
  ])
24
32
  end
25
33
 
26
- it "should add an index with prefixed columns" do
34
+ it "should add an index with prefix length" do
27
35
  @creator.add_index(["a(10)", "b"])
28
36
 
29
37
  @creator.statements.must_equal([
@@ -31,7 +39,15 @@ describe Lhm::Migrator do
31
39
  ])
32
40
  end
33
41
 
34
- it "should add an unique index" do
42
+ it "should add an index with a custom name" do
43
+ @creator.add_index([:a, :b], :custom_index_name)
44
+
45
+ @creator.statements.must_equal([
46
+ "create index `custom_index_name` on `lhmn_alt` (`a`, `b`)"
47
+ ])
48
+ end
49
+
50
+ it "should add a unique index" do
35
51
  @creator.add_unique_index(["a(5)", :b])
36
52
 
37
53
  @creator.statements.must_equal([
@@ -39,6 +55,14 @@ describe Lhm::Migrator do
39
55
  ])
40
56
  end
41
57
 
58
+ it "should add a unique index with a custom name" do
59
+ @creator.add_unique_index([:a, :b], :custom_index_name)
60
+
61
+ @creator.statements.must_equal([
62
+ "create unique index `custom_index_name` on `lhmn_alt` (`a`, `b`)"
63
+ ])
64
+ end
65
+
42
66
  it "should remove an index" do
43
67
  @creator.remove_index(["b", "a"])
44
68
 
@@ -46,6 +70,14 @@ describe Lhm::Migrator do
46
70
  "drop index `index_alt_on_b_and_a` on `lhmn_alt`"
47
71
  ])
48
72
  end
73
+
74
+ it "should remove an index with a custom name" do
75
+ @creator.remove_index([:a, :b], :custom_index_name)
76
+
77
+ @creator.statements.must_equal([
78
+ "drop index `custom_index_name` on `lhmn_alt`"
79
+ ])
80
+ end
49
81
  end
50
82
 
51
83
  describe "column changes" do
metadata CHANGED
@@ -1,10 +1,10 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: lhm
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
4
5
  prerelease:
5
- version: 1.0.3
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - SoundCloud
9
9
  - Rany Keddo
10
10
  - Tobias Bielohlawek
@@ -12,62 +12,65 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
-
16
- date: 2012-02-22 00:00:00 Z
17
- dependencies:
18
- - !ruby/object:Gem::Dependency
19
- name: mysql
20
- prerelease: false
21
- requirement: &id001 !ruby/object:Gem::Requirement
15
+ date: 2012-04-29 00:00:00.000000000 Z
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: minitest
19
+ requirement: !ruby/object:Gem::Requirement
22
20
  none: false
23
- requirements:
24
- - - ~>
25
- - !ruby/object:Gem::Version
26
- version: 2.8.1
21
+ requirements:
22
+ - - '='
23
+ - !ruby/object:Gem::Version
24
+ version: 2.10.0
27
25
  type: :development
28
- version_requirements: *id001
29
- - !ruby/object:Gem::Dependency
30
- name: minitest
31
26
  prerelease: false
32
- requirement: &id002 !ruby/object:Gem::Requirement
27
+ version_requirements: !ruby/object:Gem::Requirement
33
28
  none: false
34
- requirements:
35
- - - "="
36
- - !ruby/object:Gem::Version
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
37
32
  version: 2.10.0
38
- type: :development
39
- version_requirements: *id002
40
- - !ruby/object:Gem::Dependency
33
+ - !ruby/object:Gem::Dependency
41
34
  name: rake
42
- prerelease: false
43
- requirement: &id003 !ruby/object:Gem::Requirement
35
+ requirement: !ruby/object:Gem::Requirement
44
36
  none: false
45
- requirements:
46
- - - ">="
47
- - !ruby/object:Gem::Version
48
- version: "0"
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
49
41
  type: :development
50
- version_requirements: *id003
51
- - !ruby/object:Gem::Dependency
52
- name: activerecord
53
42
  prerelease: false
54
- requirement: &id004 !ruby/object:Gem::Requirement
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ - !ruby/object:Gem::Dependency
50
+ name: activerecord
51
+ requirement: !ruby/object:Gem::Requirement
55
52
  none: false
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: "0"
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
60
57
  type: :runtime
61
- version_requirements: *id004
62
- description: Migrate large tables without downtime by copying to a temporary table in chunks. The old table is not dropped. Instead, it is moved to timestamp_table_name for verification.
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ description: Migrate large tables without downtime by copying to a temporary table
66
+ in chunks. The old table is not dropped. Instead, it is moved to timestamp_table_name
67
+ for verification.
63
68
  email: rany@soundcloud.com, tobi@soundcloud.com, ts@soundcloud.com
64
- executables:
69
+ executables:
65
70
  - lhm-kill-queue
66
71
  extensions: []
67
-
68
72
  extra_rdoc_files: []
69
-
70
- files:
73
+ files:
71
74
  - .gitignore
72
75
  - .travis.yml
73
76
  - CHANGELOG.md
@@ -78,10 +81,13 @@ files:
78
81
  - bin/lhm-spec-clobber.sh
79
82
  - bin/lhm-spec-grants.sh
80
83
  - bin/lhm-spec-setup-cluster.sh
81
- - gemfiles/ar-2.3.gemfile
82
- - gemfiles/ar-3.1.gemfile
84
+ - bin/lhm-test-all.sh
85
+ - gemfiles/ar-2.3_mysql.gemfile
86
+ - gemfiles/ar-3.2_mysql.gemfile
87
+ - gemfiles/ar-3.2_mysql2.gemfile
83
88
  - lhm.gemspec
84
89
  - lib/lhm.rb
90
+ - lib/lhm/atomic_switcher.rb
85
91
  - lib/lhm/chunker.rb
86
92
  - lib/lhm/command.rb
87
93
  - lib/lhm/entangler.rb
@@ -100,12 +106,14 @@ files:
100
106
  - spec/fixtures/origin.ddl
101
107
  - spec/fixtures/small_table.ddl
102
108
  - spec/fixtures/users.ddl
109
+ - spec/integration/atomic_switcher_spec.rb
103
110
  - spec/integration/chunker_spec.rb
104
111
  - spec/integration/entangler_spec.rb
105
112
  - spec/integration/integration_helper.rb
106
113
  - spec/integration/lhm_spec.rb
107
114
  - spec/integration/locked_switcher_spec.rb
108
115
  - spec/integration/table_spec.rb
116
+ - spec/unit/atomic_switcher_spec.rb
109
117
  - spec/unit/chunker_spec.rb
110
118
  - spec/unit/entangler_spec.rb
111
119
  - spec/unit/intersection_spec.rb
@@ -117,44 +125,43 @@ files:
117
125
  - spec/unit/unit_helper.rb
118
126
  homepage: http://github.com/soundcloud/large-hadron-migrator
119
127
  licenses: []
120
-
121
128
  post_install_message:
122
129
  rdoc_options: []
123
-
124
- require_paths:
130
+ require_paths:
125
131
  - lib
126
- required_ruby_version: !ruby/object:Gem::Requirement
132
+ required_ruby_version: !ruby/object:Gem::Requirement
127
133
  none: false
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: "0"
132
- required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
139
  none: false
134
- requirements:
135
- - - ">="
136
- - !ruby/object:Gem::Version
137
- version: "0"
140
+ requirements:
141
+ - - ! '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
138
144
  requirements: []
139
-
140
145
  rubyforge_project:
141
- rubygems_version: 1.8.15
146
+ rubygems_version: 1.8.21
142
147
  signing_key:
143
148
  specification_version: 3
144
149
  summary: online schema changer for mysql
145
- test_files:
150
+ test_files:
146
151
  - spec/README.md
147
152
  - spec/bootstrap.rb
148
153
  - spec/fixtures/destination.ddl
149
154
  - spec/fixtures/origin.ddl
150
155
  - spec/fixtures/small_table.ddl
151
156
  - spec/fixtures/users.ddl
157
+ - spec/integration/atomic_switcher_spec.rb
152
158
  - spec/integration/chunker_spec.rb
153
159
  - spec/integration/entangler_spec.rb
154
160
  - spec/integration/integration_helper.rb
155
161
  - spec/integration/lhm_spec.rb
156
162
  - spec/integration/locked_switcher_spec.rb
157
163
  - spec/integration/table_spec.rb
164
+ - spec/unit/atomic_switcher_spec.rb
158
165
  - spec/unit/chunker_spec.rb
159
166
  - spec/unit/entangler_spec.rb
160
167
  - spec/unit/intersection_spec.rb
@@ -1,4 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- gem "activerecord", "~> 3.1.3"
4
- gemspec :path=>"../"