lhm 1.0.0.rc2 → 1.0.0.rc3
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/.config +3 -0
- data/.gitignore +1 -0
- data/.travis.yml +9 -3
- data/CHANGELOG.md +10 -0
- data/README.md +28 -24
- data/Rakefile +2 -1
- data/gemfiles/ar-2.3.gemfile +4 -0
- data/gemfiles/ar-3.1.gemfile +4 -0
- data/lhm.gemspec +1 -1
- data/lib/lhm.rb +30 -8
- data/lib/lhm/chunker.rb +53 -34
- data/lib/lhm/command.rb +15 -42
- data/lib/lhm/entangler.rb +13 -20
- data/lib/lhm/intersection.rb +3 -7
- data/lib/lhm/invoker.rb +9 -14
- data/lib/lhm/locked_switcher.rb +22 -26
- data/lib/lhm/migration.rb +2 -8
- data/lib/lhm/migrator.rb +76 -56
- data/lib/lhm/sql_helper.rb +45 -0
- data/lib/lhm/table.rb +2 -10
- data/lib/lhm/version.rb +6 -0
- data/spec/README.md +26 -0
- data/spec/bootstrap.rb +2 -5
- data/spec/config/.config +3 -0
- data/spec/config/clobber +36 -0
- data/spec/config/grants +25 -0
- data/spec/config/setup-cluster +61 -0
- data/spec/fixtures/destination.ddl +0 -1
- data/spec/fixtures/origin.ddl +0 -1
- data/spec/fixtures/users.ddl +1 -1
- data/spec/integration/chunker_spec.rb +10 -9
- data/spec/integration/entangler_spec.rb +16 -10
- data/spec/integration/integration_helper.rb +43 -12
- data/spec/integration/lhm_spec.rb +66 -42
- data/spec/integration/locked_switcher_spec.rb +11 -10
- data/spec/unit/chunker_spec.rb +50 -18
- data/spec/unit/entangler_spec.rb +2 -5
- data/spec/unit/intersection_spec.rb +2 -5
- data/spec/unit/locked_switcher_spec.rb +2 -5
- data/spec/unit/migration_spec.rb +2 -5
- data/spec/unit/migrator_spec.rb +6 -9
- data/spec/unit/sql_helper_spec.rb +32 -0
- data/spec/unit/table_spec.rb +2 -29
- data/spec/unit/unit_helper.rb +2 -5
- metadata +52 -7
- data/Gemfile +0 -3
data/.config
ADDED
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
+
before_script:
|
2
|
+
- "mysql -e 'create database lhm;'"
|
1
3
|
rvm:
|
2
4
|
- 1.8.7
|
3
|
-
- 1.9.2
|
4
5
|
- 1.9.3
|
5
|
-
|
6
|
-
-
|
6
|
+
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
|
7
13
|
branches:
|
8
14
|
only:
|
9
15
|
- master
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
# 1.0.0.rc3 (January 19, 2012)
|
2
|
+
|
3
|
+
* Speedup migrations for tables with large minimum id
|
4
|
+
* Add a bit yard documentation
|
5
|
+
* Fix issues with index creation on reserved column names
|
6
|
+
* Improve error handling
|
7
|
+
* Add tests for replication
|
8
|
+
* Rename public API method from `hadron_change_table` to `change_table`
|
9
|
+
* Add tests for ActiveRecord 2.3 and 3.1 compatibility
|
10
|
+
|
1
11
|
# 1.0.0.rc2 (January 18, 2012)
|
2
12
|
|
3
13
|
* Speedup migrations for tables with large ids
|
data/README.md
CHANGED
@@ -16,7 +16,7 @@ access becomes just as difficult.
|
|
16
16
|
|
17
17
|
There are few things that can be done at the server or engine level. It is
|
18
18
|
possible to change default values in an `ALTER TABLE` without locking the
|
19
|
-
table.
|
19
|
+
table. The InnoDB Plugin provides facilities for online index creation, which
|
20
20
|
is great if you are using this engine, but only solves half the problem.
|
21
21
|
|
22
22
|
At SoundCloud we started having migration pains quite a while ago, and after
|
@@ -42,40 +42,44 @@ twitter solution [1], it does not require the presence of an indexed
|
|
42
42
|
|
43
43
|
## Usage
|
44
44
|
|
45
|
-
|
46
|
-
|
45
|
+
You can invoke Lhm directly from a plain ruby file after connecting active
|
46
|
+
record to your mysql instance:
|
47
47
|
|
48
|
-
|
49
|
-
|
48
|
+
require 'lhm'
|
49
|
+
|
50
|
+
ActiveRecord::Base.establish_connection(
|
51
|
+
:adapter => 'mysql',
|
52
|
+
:host => '127.0.0.1',
|
53
|
+
:database => 'lhm'
|
54
|
+
)
|
55
|
+
|
56
|
+
Lhm.change_table(:users) do |m|
|
57
|
+
m.add_column(:arbitrary, "INT(12)")
|
58
|
+
m.add_index([:arbitrary, :created_at])
|
59
|
+
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
60
|
+
end
|
61
|
+
|
62
|
+
To use Lhm from an ActiveRecord::Migration in a Rails project, add it to your
|
63
|
+
Gemfile, then invoke as follows:
|
64
|
+
|
65
|
+
class MigrateUsers < ActiveRecord::Migration
|
50
66
|
|
51
67
|
def self.up
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
68
|
+
Lhm.change_table(:users) do |m|
|
69
|
+
m.add_column(:arbitrary, "INT(12)")
|
70
|
+
m.add_index([:arbitrary, :created_at])
|
71
|
+
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
56
72
|
end
|
57
73
|
end
|
58
74
|
|
59
75
|
def self.down
|
60
|
-
|
61
|
-
|
62
|
-
|
76
|
+
Lhm.change_table(:users) do |m|
|
77
|
+
m.remove_index([:arbitrary, :created_at])
|
78
|
+
m.remove_column(:arbitrary)
|
63
79
|
end
|
64
80
|
end
|
65
81
|
end
|
66
82
|
|
67
|
-
## Migration phases
|
68
|
-
|
69
|
-
_TODO_
|
70
|
-
|
71
|
-
### When adding a column
|
72
|
-
|
73
|
-
_TODO_
|
74
|
-
|
75
|
-
### When removing a column
|
76
|
-
|
77
|
-
_TODO_
|
78
|
-
|
79
83
|
## Contributing
|
80
84
|
|
81
85
|
We'll check out your contribution if you:
|
data/Rakefile
CHANGED
data/lhm.gemspec
CHANGED
data/lib/lhm.rb
CHANGED
@@ -1,20 +1,42 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
5
3
|
|
4
|
+
require 'active_record'
|
6
5
|
require 'lhm/table'
|
7
6
|
require 'lhm/invoker'
|
8
|
-
require 'lhm/
|
7
|
+
require 'lhm/version'
|
9
8
|
|
9
|
+
# Large hadron migrator - online schema change tool
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# Lhm.change_table(:users) do |m|
|
14
|
+
# m.add_column(:arbitrary, "INT(12)")
|
15
|
+
# m.add_index([:arbitrary, :created_at])
|
16
|
+
# m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
17
|
+
# end
|
18
|
+
#
|
10
19
|
module Lhm
|
11
|
-
|
20
|
+
extend self
|
12
21
|
|
13
|
-
|
22
|
+
# Alters a table with the changes described in the block
|
23
|
+
#
|
24
|
+
# @param [String, Symbol] table_name Name of the table
|
25
|
+
# @param [Hash] chunk_options Optional options to alter the chunk behavior
|
26
|
+
# @option chunk_options [Fixnum] :stride
|
27
|
+
# Size of a chunk (defaults to: 40,000)
|
28
|
+
# @option chunk_options [Fixnum] :throttle
|
29
|
+
# Time to wait between chunks in milliseconds (defaults to: 100)
|
30
|
+
# @yield [Migrator] Yielded Migrator object records the changes
|
31
|
+
# @return [Boolean] Returns true if the migration finishs
|
32
|
+
# @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
|
33
|
+
def change_table(table_name, chunk_options = {}, &block)
|
34
|
+
connection = ActiveRecord::Base.connection
|
14
35
|
origin = Table.parse(table_name, connection)
|
15
36
|
invoker = Invoker.new(origin, connection)
|
16
37
|
block.call(invoker.migrator)
|
17
38
|
invoker.run(chunk_options)
|
39
|
+
|
40
|
+
true
|
18
41
|
end
|
19
42
|
end
|
20
|
-
|
data/lib/lhm/chunker.rb
CHANGED
@@ -1,64 +1,88 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
5
3
|
|
6
|
-
require 'lhm/migration'
|
7
4
|
require 'lhm/command'
|
5
|
+
require 'lhm/sql_helper'
|
8
6
|
|
9
7
|
module Lhm
|
10
8
|
class Chunker
|
11
9
|
include Command
|
10
|
+
include SqlHelper
|
11
|
+
|
12
|
+
attr_reader :connection
|
12
13
|
|
13
|
-
#
|
14
14
|
# Copy from origin to destination in chunks of size `stride`. Sleeps for
|
15
15
|
# `throttle` milliseconds between each stride.
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def initialize(migration, connection = nil, options = {})
|
17
|
+
@migration = migration
|
18
|
+
@connection = connection
|
19
19
|
@stride = options[:stride] || 40_000
|
20
20
|
@throttle = options[:throttle] || 100
|
21
|
-
@
|
22
|
-
@
|
23
|
-
@migration = migration
|
21
|
+
@start = options[:start] || select_start
|
22
|
+
@limit = options[:limit] || select_limit
|
24
23
|
end
|
25
24
|
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def up_to(limit)
|
31
|
-
traversable_chunks_up_to(limit).times do |n|
|
32
|
-
yield(bottom(n + 1), top(n + 1, limit))
|
25
|
+
# Copies chunks of size `stride`, starting from `start` up to id `limit`.
|
26
|
+
def up_to(&block)
|
27
|
+
1.upto(traversable_chunks_size) do |n|
|
28
|
+
yield(bottom(n), top(n))
|
33
29
|
end
|
34
30
|
end
|
35
31
|
|
36
|
-
def
|
37
|
-
(limit / @stride.to_f).ceil
|
32
|
+
def traversable_chunks_size
|
33
|
+
@limit && @start ? ((@limit - @start + 1) / @stride.to_f).ceil : 0
|
38
34
|
end
|
39
35
|
|
40
36
|
def bottom(chunk)
|
41
|
-
(chunk - 1) * @stride +
|
37
|
+
(chunk - 1) * @stride + @start
|
42
38
|
end
|
43
39
|
|
44
|
-
def top(chunk
|
45
|
-
[chunk * @stride, limit].min
|
40
|
+
def top(chunk)
|
41
|
+
[chunk * @stride + @start - 1, @limit].min
|
46
42
|
end
|
47
43
|
|
48
44
|
def copy(lowest, highest)
|
49
|
-
"insert ignore into `#{
|
50
|
-
"select #{
|
45
|
+
"insert ignore into `#{ destination_name }` (#{ columns }) " +
|
46
|
+
"select #{ columns } from `#{ origin_name }` " +
|
51
47
|
"where `id` between #{ lowest } and #{ highest }"
|
52
48
|
end
|
53
49
|
|
50
|
+
def select_start
|
51
|
+
start = connection.select_value("select min(id) from #{ origin_name }")
|
52
|
+
start ? start.to_i : nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def select_limit
|
56
|
+
limit = connection.select_value("select max(id) from #{ origin_name }")
|
57
|
+
limit ? limit.to_i : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def throttle_seconds
|
61
|
+
@throttle / 1000.0
|
62
|
+
end
|
63
|
+
|
54
64
|
private
|
55
65
|
|
56
|
-
def
|
57
|
-
@
|
66
|
+
def destination_name
|
67
|
+
@migration.destination.name
|
68
|
+
end
|
69
|
+
|
70
|
+
def origin_name
|
71
|
+
@migration.origin.name
|
72
|
+
end
|
73
|
+
|
74
|
+
def columns
|
75
|
+
@columns ||= @migration.intersection.joined
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate
|
79
|
+
if @start && @limit && @start > @limit
|
80
|
+
error("impossible chunk options (limit must be greater than start)")
|
81
|
+
end
|
58
82
|
end
|
59
83
|
|
60
84
|
def execute
|
61
|
-
up_to
|
85
|
+
up_to do |lowest, highest|
|
62
86
|
affected_rows = update(copy(lowest, highest))
|
63
87
|
|
64
88
|
if affected_rows > 0
|
@@ -68,10 +92,5 @@ module Lhm
|
|
68
92
|
print "."
|
69
93
|
end
|
70
94
|
end
|
71
|
-
|
72
|
-
def throttle_seconds
|
73
|
-
@throttle / 1000.0
|
74
|
-
end
|
75
95
|
end
|
76
96
|
end
|
77
|
-
|
data/lib/lhm/command.rb
CHANGED
@@ -1,24 +1,11 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
5
|
-
# Apply a change to the database.
|
6
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
7
3
|
|
8
4
|
module Lhm
|
9
|
-
|
10
|
-
|
11
|
-
base.send :attr_reader, :connection
|
12
|
-
end
|
13
|
-
|
14
|
-
#
|
15
|
-
# Command Interface
|
16
|
-
#
|
17
|
-
|
18
|
-
def validate; end
|
19
|
-
|
20
|
-
def revert; end
|
5
|
+
class Error < StandardError
|
6
|
+
end
|
21
7
|
|
8
|
+
module Command
|
22
9
|
def run(&block)
|
23
10
|
validate
|
24
11
|
|
@@ -29,45 +16,31 @@ module Lhm
|
|
29
16
|
else
|
30
17
|
execute
|
31
18
|
end
|
19
|
+
rescue
|
20
|
+
revert
|
21
|
+
raise
|
32
22
|
end
|
33
23
|
|
34
24
|
private
|
35
25
|
|
36
|
-
def
|
37
|
-
raise NotImplementedError.new(self.class.name)
|
26
|
+
def validate
|
38
27
|
end
|
39
28
|
|
40
|
-
def
|
41
|
-
raise NotImplementedError.new(self.class.name)
|
29
|
+
def revert
|
42
30
|
end
|
43
31
|
|
44
|
-
def
|
32
|
+
def execute
|
45
33
|
raise NotImplementedError.new(self.class.name)
|
46
34
|
end
|
47
35
|
|
48
|
-
def
|
49
|
-
@connection.table_exists?(table_name)
|
50
|
-
end
|
51
|
-
|
52
|
-
def error(msg)
|
53
|
-
raise Exception.new("#{ self.class }: #{ msg }")
|
36
|
+
def before
|
54
37
|
end
|
55
38
|
|
56
|
-
def
|
57
|
-
[statements].flatten.each { |statement| @connection.execute(statement) }
|
58
|
-
rescue ActiveRecord::StatementInvalid, Mysql::Error => e
|
59
|
-
revert
|
60
|
-
error e.message
|
39
|
+
def after
|
61
40
|
end
|
62
41
|
|
63
|
-
def
|
64
|
-
|
65
|
-
memo += @connection.update(statement)
|
66
|
-
end
|
67
|
-
rescue ActiveRecord::StatementInvalid, Mysql::Error => e
|
68
|
-
revert
|
69
|
-
error e.message
|
42
|
+
def error(msg)
|
43
|
+
raise Error.new(msg)
|
70
44
|
end
|
71
45
|
end
|
72
46
|
end
|
73
|
-
|
data/lib/lhm/entangler.rb
CHANGED
@@ -1,19 +1,18 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
5
|
-
# Creates entanglement between two tables. All creates, updates and deletes
|
6
|
-
# to origin will be repeated on the the destination table.
|
7
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
8
3
|
|
9
4
|
require 'lhm/command'
|
5
|
+
require 'lhm/sql_helper'
|
10
6
|
|
11
7
|
module Lhm
|
12
8
|
class Entangler
|
13
9
|
include Command
|
10
|
+
include SqlHelper
|
14
11
|
|
15
|
-
attr_reader :
|
12
|
+
attr_reader :connection
|
16
13
|
|
14
|
+
# Creates entanglement between two tables. All creates, updates and deletes
|
15
|
+
# to origin will be repeated on the destination table.
|
17
16
|
def initialize(migration, connection = nil)
|
18
17
|
@common = migration.intersection
|
19
18
|
@origin = migration.origin
|
@@ -23,9 +22,9 @@ module Lhm
|
|
23
22
|
|
24
23
|
def entangle
|
25
24
|
[
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
create_delete_trigger,
|
26
|
+
create_insert_trigger,
|
27
|
+
create_update_trigger
|
29
28
|
]
|
30
29
|
end
|
31
30
|
|
@@ -37,7 +36,7 @@ module Lhm
|
|
37
36
|
]
|
38
37
|
end
|
39
38
|
|
40
|
-
def
|
39
|
+
def create_insert_trigger
|
41
40
|
strip %Q{
|
42
41
|
create trigger `#{ trigger(:ins) }`
|
43
42
|
after insert on `#{ @origin.name }` for each row
|
@@ -46,7 +45,7 @@ module Lhm
|
|
46
45
|
}
|
47
46
|
end
|
48
47
|
|
49
|
-
def
|
48
|
+
def create_update_trigger
|
50
49
|
strip %Q{
|
51
50
|
create trigger `#{ trigger(:upd) }`
|
52
51
|
after update on `#{ @origin.name }` for each row
|
@@ -55,7 +54,7 @@ module Lhm
|
|
55
54
|
}
|
56
55
|
end
|
57
56
|
|
58
|
-
def
|
57
|
+
def create_delete_trigger
|
59
58
|
strip %Q{
|
60
59
|
create trigger `#{ trigger(:del) }`
|
61
60
|
after delete on `#{ @origin.name }` for each row
|
@@ -68,10 +67,6 @@ module Lhm
|
|
68
67
|
"lhmt_#{ type }_#{ @origin.name }"
|
69
68
|
end
|
70
69
|
|
71
|
-
#
|
72
|
-
# Command implementation
|
73
|
-
#
|
74
|
-
|
75
70
|
def validate
|
76
71
|
unless table?(@origin.name)
|
77
72
|
error("#{ @origin.name } does not exist")
|
@@ -84,7 +79,6 @@ module Lhm
|
|
84
79
|
|
85
80
|
def before
|
86
81
|
sql(entangle)
|
87
|
-
@epoch = connection.select_value("select max(id) from #{ @origin.name }").to_i
|
88
82
|
end
|
89
83
|
|
90
84
|
def after
|
@@ -102,4 +96,3 @@ module Lhm
|
|
102
96
|
end
|
103
97
|
end
|
104
98
|
end
|
105
|
-
|