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 +2 -0
- data/.travis.yml +4 -1
- data/README.md +47 -8
- data/gemfiles/ar-2.3_mysql.gemfile +1 -1
- data/gemfiles/ar-3.2_mysql.gemfile +1 -1
- data/gemfiles/ar-3.2_mysql2.gemfile +1 -1
- data/gemfiles/dm_mysql.gemfile +1 -1
- data/lhm.gemspec +1 -1
- data/lib/lhm.rb +5 -2
- data/lib/lhm/chunker.rb +10 -2
- data/lib/lhm/migration.rb +3 -2
- data/lib/lhm/migrator.rb +21 -2
- data/lib/lhm/version.rb +1 -1
- data/spec/fixtures/permissions.ddl +5 -0
- data/spec/fixtures/tracks.ddl +5 -0
- data/spec/fixtures/users.ddl +3 -1
- data/spec/integration/lhm_spec.rb +52 -2
- data/spec/unit/chunker_spec.rb +2 -2
- data/spec/unit/migration_spec.rb +1 -1
- metadata +35 -4
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Large Hadron Migrator [![Build Status]
|
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
|
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]:
|
225
|
+
[4]: https://travis-ci.org/soundcloud/lhm
|
226
|
+
[5]: https://travis-ci.org/soundcloud/lhm.png?branch=master
|
data/gemfiles/dm_mysql.gemfile
CHANGED
data/lhm.gemspec
CHANGED
@@ -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/
|
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
|
data/lib/lhm/chunker.rb
CHANGED
@@ -43,8 +43,8 @@ module Lhm
|
|
43
43
|
|
44
44
|
def copy(lowest, highest)
|
45
45
|
"insert ignore into `#{ destination_name }` (#{ columns }) " +
|
46
|
-
"select #{
|
47
|
-
"
|
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)")
|
data/lib/lhm/migration.rb
CHANGED
@@ -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
|
|
data/lib/lhm/migrator.rb
CHANGED
@@ -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
|
data/lib/lhm/version.rb
CHANGED
data/spec/fixtures/users.ddl
CHANGED
@@ -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(:
|
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, :
|
160
|
+
index?(:users, :index_with_a_custom_name).must_equal(false)
|
111
161
|
end
|
112
162
|
end
|
113
163
|
|
data/spec/unit/chunker_spec.rb
CHANGED
@@ -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
|
30
|
-
"where
|
29
|
+
"select origin.`secret` from `origin` " +
|
30
|
+
"where origin.`id` between 1 and 100"
|
31
31
|
)
|
32
32
|
end
|
33
33
|
end
|
data/spec/unit/migration_spec.rb
CHANGED
@@ -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:
|
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-
|
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/
|
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
|