lhm 1.3.0 → 2.0.0
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.
- 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
|