lhm 1.3.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,5 @@ Gemfile.lock
4
4
  gemfiles/*.lock
5
5
  pkg/*
6
6
  .rvmrc
7
+ .ruby-version
8
+ .ruby-gemset
@@ -2,8 +2,11 @@ language: ruby
2
2
  before_script:
3
3
  - "mysql -e 'create database lhm;'"
4
4
  rvm:
5
- - 1.8.7
6
5
  - 1.9.3
6
+ - 2.0.0
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: 2.0.0
7
10
  gemfile:
8
11
  - gemfiles/ar-2.3_mysql.gemfile
9
12
  - gemfiles/ar-3.2_mysql.gemfile
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png?branch=master)][4]
1
+ # Large Hadron Migrator [![Build Status][5]][4]
2
2
 
3
3
  Rails style database migrations are a useful way to evolve your data schema in
4
4
  an agile manner. Most Rails projects start like this, and at first, making
@@ -45,13 +45,16 @@ the [twitter solution][2], it does not require the presence of an indexed
45
45
  Lhm currently only works with MySQL databases and requires an established
46
46
  ActiveRecord or DataMapper connection.
47
47
 
48
- It is compatible and [continuously tested][4] with Ruby 1.8.7 and Ruby 1.9.x,
48
+ It is compatible and [continuously tested][4] with MRI 1.9.x,
49
49
  ActiveRecord 2.3.x and 3.x (mysql and mysql2 adapters), as well as DataMapper
50
50
  1.2 (dm-mysql-adapter).
51
51
 
52
52
  Lhm also works with dm-master-slave-adapter, it'll bind to the master before
53
53
  running the migrations.
54
54
 
55
+ The test suite is also run against MRI 2.0.0 in Continuous Integration, but
56
+ there are a few bugs left to fix.
57
+
55
58
  ## Limitations
56
59
 
57
60
  Lhm requires a monotonically increasing numeric Primary Key on the table, due to how
@@ -143,13 +146,13 @@ to prevent accidental data loss.
143
146
  There are two different table rename strategies available: LockedSwitcher and
144
147
  AtomicSwitcher.
145
148
 
146
- The LockedSwitcher strategy locks the table being migrated and issues two ALTER TABLE statements.
149
+ The LockedSwitcher strategy locks the table being migrated and issues two ALTER TABLE statements.
147
150
  The AtomicSwitcher uses a single atomic RENAME TABLE query and is the favored solution.
148
151
 
149
- Lhm chooses AtomicSwitcher if no strategy is specified, **unless** your version of MySQL is
150
- affected by [binlog bug #39675](http://bugs.mysql.com/bug.php?id=39675). If your version is
151
- affected, Lhm will raise an error if you don't specify a strategy. You're recommended
152
- to use the LockedSwitcher in these cases to avoid replication issues.
152
+ Lhm chooses AtomicSwitcher if no strategy is specified, **unless** your version of MySQL is
153
+ affected by [binlog bug #39675](http://bugs.mysql.com/bug.php?id=39675). If your version is
154
+ affected, Lhm will raise an error if you don't specify a strategy. You're recommended
155
+ to use the LockedSwitcher in these cases to avoid replication issues.
153
156
 
154
157
  To specify the strategy in your migration:
155
158
 
@@ -159,6 +162,41 @@ Lhm.change_table :users, :atomic_switch => true do |m|
159
162
  end
160
163
  ```
161
164
 
165
+ ## Limiting the data that is migrated
166
+
167
+ For instances where you want to limit the data that is migrated to the new
168
+ table by some conditions, you may tell the migration to filter by a set of
169
+ conditions:
170
+
171
+ ```ruby
172
+ Lhm.change_table(:sounds) do |m|
173
+ m.filter("inner join users on users.`id` = sounds.`user_id` and sounds.`public` = 1")
174
+ end
175
+ ```
176
+
177
+ Note that this SQL will be inserted into the copy directly after the "from"
178
+ statement - so be sure to use inner/outer join syntax and not cross joins. These
179
+ conditions will not affect the triggers, so any modifications to the table
180
+ during the run will happen on the new table as well.
181
+
182
+ ## Cleaning up after an interrupted Lhm run
183
+
184
+ If an Lhm migration is interrupted, it may leave behind the temporary tables
185
+ used in the migration. If the migration is re-started, the unexpected presence
186
+ of these tables will cause an error. In this case, `Lhm.cleanup` can be used
187
+ to drop any orphaned Lhm temporary tables.
188
+
189
+ To see what Lhm tables are found:
190
+
191
+ ```ruby
192
+ Lhm.cleanup
193
+ ```
194
+
195
+ To remove any Lhm tables found:
196
+ ```ruby
197
+ Lhm.cleanup(true)
198
+ ```
199
+
162
200
  ## Contributing
163
201
 
164
202
  We'll check out your contribution if you:
@@ -184,4 +222,5 @@ The license is included as LICENSE in this directory.
184
222
  [1]: http://www.facebook.com/note.php?note\_id=430801045932
185
223
  [2]: https://github.com/freels/table_migrator
186
224
  [3]: http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html
187
- [4]: http://travis-ci.org/soundcloud/large-hadron-migrator
225
+ [4]: https://travis-ci.org/soundcloud/lhm
226
+ [5]: https://travis-ci.org/soundcloud/lhm.png?branch=master
@@ -1,4 +1,4 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
 
3
3
  gem "mysql", "~> 2.8.1"
4
4
  gem "activerecord", "~> 2.3.14"
@@ -1,4 +1,4 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
 
3
3
  gem "mysql", "~> 2.8.1"
4
4
  gem "activerecord", "~> 3.2.2"
@@ -1,4 +1,4 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
 
3
3
  gem "mysql2", "~> 0.3.11"
4
4
  gem "activerecord", "~> 3.2.2"
@@ -1,4 +1,4 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
 
3
3
  gem 'dm-core'
4
4
  gem 'dm-mysql-adapter'
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
13
13
  s.email = %q{rany@soundcloud.com, tobi@soundcloud.com, ts@soundcloud.com}
14
14
  s.summary = %q{online schema changer for mysql}
15
15
  s.description = %q{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.}
16
- s.homepage = %q{http://github.com/soundcloud/large-hadron-migrator}
16
+ s.homepage = %q{http://github.com/soundcloud/lhm}
17
17
  s.files = `git ls-files`.split("\n")
18
18
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
19
  s.require_paths = ["lib"]
data/lib/lhm.rb CHANGED
@@ -26,6 +26,10 @@ module Lhm
26
26
  # Size of a chunk (defaults to: 40,000)
27
27
  # @option options [Fixnum] :throttle
28
28
  # Time to wait between chunks in milliseconds (defaults to: 100)
29
+ # @option options [Fixnum] :start
30
+ # Primary Key position at which to start copying chunks
31
+ # @option options [Fixnum] :limit
32
+ # Primary Key position at which to stop copying chunks
29
33
  # @option options [Boolean] :atomic_switch
30
34
  # Use atomic switch to rename tables (defaults to: true)
31
35
  # If using a version of mysql affected by atomic switch bug, LHM forces user
@@ -38,7 +42,6 @@ module Lhm
38
42
  invoker = Invoker.new(origin, connection)
39
43
  block.call(invoker.migrator)
40
44
  invoker.run(options)
41
-
42
45
  true
43
46
  end
44
47
 
@@ -72,8 +75,8 @@ module Lhm
72
75
  end
73
76
 
74
77
  protected
78
+
75
79
  def self.connection
76
80
  Connection.new(adapter)
77
81
  end
78
-
79
82
  end
@@ -43,8 +43,8 @@ module Lhm
43
43
 
44
44
  def copy(lowest, highest)
45
45
  "insert ignore into `#{ destination_name }` (#{ columns }) " +
46
- "select #{ columns } from `#{ origin_name }` " +
47
- "where `id` between #{ lowest } and #{ highest }"
46
+ "select #{ select_columns } from `#{ origin_name }` " +
47
+ "#{ conditions } #{ origin_name }.`id` between #{ lowest } and #{ highest }"
48
48
  end
49
49
 
50
50
  def select_start
@@ -63,6 +63,10 @@ module Lhm
63
63
 
64
64
  private
65
65
 
66
+ def conditions
67
+ @migration.conditions ? "#{@migration.conditions} and" : "where"
68
+ end
69
+
66
70
  def destination_name
67
71
  @migration.destination.name
68
72
  end
@@ -75,6 +79,10 @@ module Lhm
75
79
  @columns ||= @migration.intersection.joined
76
80
  end
77
81
 
82
+ def select_columns
83
+ @select_columns ||= @migration.intersection.typed(origin_name)
84
+ end
85
+
78
86
  def validate
79
87
  if @start && @limit && @start > @limit
80
88
  error("impossible chunk options (limit must be greater than start)")
@@ -5,11 +5,12 @@ require 'lhm/intersection'
5
5
 
6
6
  module Lhm
7
7
  class Migration
8
- attr_reader :origin, :destination
8
+ attr_reader :origin, :destination, :conditions
9
9
 
10
- def initialize(origin, destination, time = Time.now)
10
+ def initialize(origin, destination, conditions = nil, time = Time.now)
11
11
  @origin = origin
12
12
  @destination = destination
13
+ @conditions = conditions
13
14
  @start = time
14
15
  end
15
16
 
@@ -13,7 +13,7 @@ module Lhm
13
13
  include Command
14
14
  include SqlHelper
15
15
 
16
- attr_reader :name, :statements, :connection
16
+ attr_reader :name, :statements, :connection, :conditions
17
17
 
18
18
  def initialize(table, connection = nil)
19
19
  @connection = connection
@@ -135,10 +135,29 @@ module Lhm
135
135
  # @param [String, Symbol] index_name
136
136
  # Optional name of the index to be removed
137
137
  def remove_index(columns, index_name = nil)
138
+ columns = [columns].flatten.map(&:to_sym)
139
+ from_origin = @origin.indices.find {|name, cols| cols.map(&:to_sym) == columns}
140
+ index_name ||= from_origin[0] unless from_origin.nil?
138
141
  index_name ||= idx_name(@origin.name, columns)
139
142
  ddl("drop index `%s` on `%s`" % [index_name, @name])
140
143
  end
141
144
 
145
+ # Filter the data that is copied into the new table by the provided SQL.
146
+ # This SQL will be inserted into the copy directly after the "from"
147
+ # statement - so be sure to use inner/outer join syntax and not cross joins.
148
+ #
149
+ # @example Add a conditions filter to the migration.
150
+ # Lhm.change_table(:sounds) do |m|
151
+ # m.filter("inner join users on users.`id` = sounds.`user_id` and sounds.`public` = 1")
152
+ # end
153
+ #
154
+ # @param [ String ] sql The sql filter.
155
+ #
156
+ # @return [ String ] The sql filter.
157
+ def filter(sql)
158
+ @conditions = sql
159
+ end
160
+
142
161
  private
143
162
 
144
163
  def validate
@@ -160,7 +179,7 @@ module Lhm
160
179
  def execute
161
180
  destination_create
162
181
  @connection.sql(@statements)
163
- Migration.new(@origin, destination_read)
182
+ Migration.new(@origin, destination_read, conditions)
164
183
  end
165
184
 
166
185
  def destination_create
@@ -2,5 +2,5 @@
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
5
- VERSION = "1.3.0"
5
+ VERSION = "2.0.0"
6
6
  end
@@ -0,0 +1,5 @@
1
+ CREATE TABLE `permissions` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `track_id` int(11) DEFAULT NULL,
4
+ PRIMARY KEY (`id`)
5
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,5 @@
1
+ CREATE TABLE `tracks` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `public` int(4) DEFAULT 0,
4
+ PRIMARY KEY (`id`)
5
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -8,5 +8,7 @@ CREATE TABLE `users` (
8
8
  `description` text,
9
9
  PRIMARY KEY (`id`),
10
10
  UNIQUE KEY `index_users_on_reference` (`reference`),
11
- KEY `index_users_on_username_and_created_at` (`username`,`created_at`)
11
+ KEY `index_users_on_username_and_created_at` (`username`,`created_at`),
12
+ KEY `index_with_a_custom_name` (`username`,`group`)
13
+
12
14
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -13,6 +13,46 @@ describe Lhm do
13
13
  describe "changes" do
14
14
  before(:each) do
15
15
  table_create(:users)
16
+ table_create(:tracks)
17
+ table_create(:permissions)
18
+ end
19
+
20
+ describe "when providing a subset of data to copy" do
21
+
22
+ before do
23
+ execute("insert into tracks set id = 13, public = 0")
24
+ 11.times { |n| execute("insert into tracks set id = #{n + 1}, public = 1") }
25
+ 11.times { |n| execute("insert into permissions set track_id = #{n + 1}") }
26
+
27
+ Lhm.change_table(:permissions, :atomic_switch => false) do |t|
28
+ t.filter("inner join tracks on tracks.`id` = permissions.`track_id` and tracks.`public` = 1")
29
+ end
30
+ end
31
+
32
+ describe "when no additional data is inserted into the table" do
33
+
34
+ it "migrates the existing data" do
35
+ slave do
36
+ count_all(:permissions).must_equal(11)
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "when additional data is inserted" do
42
+
43
+ before do
44
+ execute("insert into tracks set id = 14, public = 0")
45
+ execute("insert into tracks set id = 15, public = 1")
46
+ execute("insert into permissions set track_id = 14")
47
+ execute("insert into permissions set track_id = 15")
48
+ end
49
+
50
+ it "migrates all data" do
51
+ slave do
52
+ count_all(:permissions).must_equal(13)
53
+ end
54
+ end
55
+ end
16
56
  end
17
57
 
18
58
  it "should add a column" do
@@ -103,11 +143,21 @@ describe Lhm do
103
143
 
104
144
  it "should remove an index with a custom name" do
105
145
  Lhm.change_table(:users, :atomic_switch => false) do |t|
106
- t.remove_index(:reference, :index_users_on_reference)
146
+ t.remove_index([:username, :group])
147
+ end
148
+
149
+ slave do
150
+ index?(:users, :index_with_a_custom_name).must_equal(false)
151
+ end
152
+ end
153
+
154
+ it "should remove an index with a custom name by name" do
155
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
156
+ t.remove_index(:irrelevant_column_name, :index_with_a_custom_name)
107
157
  end
108
158
 
109
159
  slave do
110
- index?(:users, :index_users_on_reference).must_equal(false)
160
+ index?(:users, :index_with_a_custom_name).must_equal(false)
111
161
  end
112
162
  end
113
163
 
@@ -26,8 +26,8 @@ describe Lhm::Chunker do
26
26
  it "should copy the correct range and column" do
27
27
  @chunker.copy(from = 1, to = 100).must_equal(
28
28
  "insert ignore into `destination` (`secret`) " +
29
- "select `secret` from `origin` " +
30
- "where `id` between 1 and 100"
29
+ "select origin.`secret` from `origin` " +
30
+ "where origin.`id` between 1 and 100"
31
31
  )
32
32
  end
33
33
  end
@@ -13,7 +13,7 @@ describe Lhm::Migration do
13
13
  @start = Time.now
14
14
  @origin = Lhm::Table.new("origin")
15
15
  @destination = Lhm::Table.new("destination")
16
- @migration = Lhm::Migration.new(@origin, @destination, @start)
16
+ @migration = Lhm::Migration.new(@origin, @destination, nil, @start)
17
17
  end
18
18
 
19
19
  it "should name archive" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lhm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 2.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2013-05-28 00:00:00.000000000 Z
15
+ date: 2013-07-10 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: minitest
@@ -90,7 +90,9 @@ files:
90
90
  - spec/bootstrap.rb
91
91
  - spec/fixtures/destination.ddl
92
92
  - spec/fixtures/origin.ddl
93
+ - spec/fixtures/permissions.ddl
93
94
  - spec/fixtures/small_table.ddl
95
+ - spec/fixtures/tracks.ddl
94
96
  - spec/fixtures/users.ddl
95
97
  - spec/integration/atomic_switcher_spec.rb
96
98
  - spec/integration/chunker_spec.rb
@@ -113,7 +115,7 @@ files:
113
115
  - spec/unit/sql_helper_spec.rb
114
116
  - spec/unit/table_spec.rb
115
117
  - spec/unit/unit_helper.rb
116
- homepage: http://github.com/soundcloud/large-hadron-migrator
118
+ homepage: http://github.com/soundcloud/lhm
117
119
  licenses: []
118
120
  post_install_message:
119
121
  rdoc_options: []
@@ -137,4 +139,33 @@ rubygems_version: 1.8.25
137
139
  signing_key:
138
140
  specification_version: 3
139
141
  summary: online schema changer for mysql
140
- test_files: []
142
+ test_files:
143
+ - spec/README.md
144
+ - spec/bootstrap.rb
145
+ - spec/fixtures/destination.ddl
146
+ - spec/fixtures/origin.ddl
147
+ - spec/fixtures/permissions.ddl
148
+ - spec/fixtures/small_table.ddl
149
+ - spec/fixtures/tracks.ddl
150
+ - spec/fixtures/users.ddl
151
+ - spec/integration/atomic_switcher_spec.rb
152
+ - spec/integration/chunker_spec.rb
153
+ - spec/integration/cleanup_spec.rb
154
+ - spec/integration/entangler_spec.rb
155
+ - spec/integration/integration_helper.rb
156
+ - spec/integration/lhm_spec.rb
157
+ - spec/integration/locked_switcher_spec.rb
158
+ - spec/integration/table_spec.rb
159
+ - spec/unit/active_record_connection_spec.rb
160
+ - spec/unit/atomic_switcher_spec.rb
161
+ - spec/unit/chunker_spec.rb
162
+ - spec/unit/connection_spec.rb
163
+ - spec/unit/datamapper_connection_spec.rb
164
+ - spec/unit/entangler_spec.rb
165
+ - spec/unit/intersection_spec.rb
166
+ - spec/unit/locked_switcher_spec.rb
167
+ - spec/unit/migration_spec.rb
168
+ - spec/unit/migrator_spec.rb
169
+ - spec/unit/sql_helper_spec.rb
170
+ - spec/unit/table_spec.rb
171
+ - spec/unit/unit_helper.rb