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 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