zdm 1.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.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.travis.yml +19 -0
- data/Appraisals +9 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +18 -0
- data/Rakefile +11 -0
- data/gemfiles/4.1.gemfile +7 -0
- data/gemfiles/4.1.gemfile.lock +66 -0
- data/gemfiles/4.2.gemfile +7 -0
- data/gemfiles/4.2.gemfile.lock +64 -0
- data/gemfiles/5.0.gemfile +7 -0
- data/gemfiles/5.0.gemfile.lock +63 -0
- data/lib/version.rb +3 -0
- data/lib/zdm.rb +268 -0
- data/spec/database.yml +21 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/zdm_spec.rb +88 -0
- data/zdm.gemspec +26 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c4711654e1f0b53c22c8bf17f27c8d89b9667112
|
4
|
+
data.tar.gz: 43df95e14936ae972174b70852cdaad748fb34e2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 167fdd1568bc0227c89f12f49ce956da2b7cd990b87458a13c1bd5cd96722c8d0e811fad86092f2a3ed7def58ee56ca59e39eb5abaae370924681435c77d364b
|
7
|
+
data.tar.gz: 866bb805124b34d25d104dd6325ba8ade8668569f08c01588bf4a91d9c7a245aac79849e1c7ad4113385fe00a0d025d1a2b6315f751407b152badcd85df2be68
|
data/.gitignore
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
## Environment normalization:
|
14
|
+
/.bundle/
|
15
|
+
/vendor/bundle
|
16
|
+
/lib/bundler/man/
|
17
|
+
|
18
|
+
# for a library or gem, you might want to ignore these files since the code is
|
19
|
+
# intended to run in multiple environments; otherwise, check them in:
|
20
|
+
Gemfile.lock
|
21
|
+
# .ruby-version
|
22
|
+
# .ruby-gemset
|
23
|
+
|
24
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
25
|
+
.rvmrc
|
26
|
+
|
27
|
+
*.sh
|
data/.travis.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
rvm:
|
4
|
+
- 2.3.0
|
5
|
+
- 2.4.0
|
6
|
+
|
7
|
+
sudo: false
|
8
|
+
|
9
|
+
gemfile:
|
10
|
+
- gemfiles/4.1.gemfile
|
11
|
+
- gemfiles/4.2.gemfile
|
12
|
+
- gemfiles/5.0.gemfile
|
13
|
+
|
14
|
+
services:
|
15
|
+
- mysql
|
16
|
+
before_install:
|
17
|
+
- mysql -e 'CREATE DATABASE zdm_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
|
18
|
+
|
19
|
+
script: 'bundle exec rake'
|
data/Appraisals
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 ITRP
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Zero Downtime Migrator
|
2
|
+
|
3
|
+
Minimal code to migrate big tables in mysql, mariadb or aurora with zero downtime of the systems.
|
4
|
+
Only works with tables that have an auto increment primary key column named `id`.
|
5
|
+
|
6
|
+
Read [Facebook's OCS](https://www.facebook.com/note.php?note_id=430801045932) commentary.
|
7
|
+
Instead of using outfiles we follow [lhm](https://github.com/soundcloud/lhm)'s approach.
|
8
|
+
|
9
|
+
The code is the readme. If you donot grok the code then you really should not use this.
|
10
|
+
|
11
|
+
Install
|
12
|
+
=======
|
13
|
+
|
14
|
+
```
|
15
|
+
gem install zdm
|
16
|
+
```
|
17
|
+
|
18
|
+
[](https://travis-ci.org/itrp/zdm)
|
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
zdm (1.0.0)
|
5
|
+
activerecord (>= 4.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (4.1.15)
|
11
|
+
activesupport (= 4.1.15)
|
12
|
+
builder (~> 3.1)
|
13
|
+
activerecord (4.1.15)
|
14
|
+
activemodel (= 4.1.15)
|
15
|
+
activesupport (= 4.1.15)
|
16
|
+
arel (~> 5.0.0)
|
17
|
+
activesupport (4.1.15)
|
18
|
+
i18n (~> 0.6, >= 0.6.9)
|
19
|
+
json (~> 1.7, >= 1.7.7)
|
20
|
+
minitest (~> 5.1)
|
21
|
+
thread_safe (~> 0.1)
|
22
|
+
tzinfo (~> 1.1)
|
23
|
+
appraisal (2.1.0)
|
24
|
+
bundler
|
25
|
+
rake
|
26
|
+
thor (>= 0.14.0)
|
27
|
+
arel (5.0.1.20140414130214)
|
28
|
+
builder (3.2.3)
|
29
|
+
diff-lcs (1.3)
|
30
|
+
i18n (0.8.1)
|
31
|
+
json (1.8.6)
|
32
|
+
minitest (5.10.1)
|
33
|
+
mysql2 (0.3.20)
|
34
|
+
rake (12.0.0)
|
35
|
+
rspec (3.5.0)
|
36
|
+
rspec-core (~> 3.5.0)
|
37
|
+
rspec-expectations (~> 3.5.0)
|
38
|
+
rspec-mocks (~> 3.5.0)
|
39
|
+
rspec-core (3.5.4)
|
40
|
+
rspec-support (~> 3.5.0)
|
41
|
+
rspec-expectations (3.5.0)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.5.0)
|
44
|
+
rspec-mocks (3.5.0)
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
+
rspec-support (~> 3.5.0)
|
47
|
+
rspec-support (3.5.0)
|
48
|
+
thor (0.19.4)
|
49
|
+
thread_safe (0.3.6)
|
50
|
+
tzinfo (1.2.2)
|
51
|
+
thread_safe (~> 0.1)
|
52
|
+
|
53
|
+
PLATFORMS
|
54
|
+
ruby
|
55
|
+
|
56
|
+
DEPENDENCIES
|
57
|
+
activerecord (= 4.1.15)
|
58
|
+
appraisal
|
59
|
+
bundler (~> 1)
|
60
|
+
mysql2
|
61
|
+
rake
|
62
|
+
rspec
|
63
|
+
zdm!
|
64
|
+
|
65
|
+
BUNDLED WITH
|
66
|
+
1.14.5
|
@@ -0,0 +1,64 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
zdm (1.0.0)
|
5
|
+
activerecord (>= 4.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (4.2.8)
|
11
|
+
activesupport (= 4.2.8)
|
12
|
+
builder (~> 3.1)
|
13
|
+
activerecord (4.2.8)
|
14
|
+
activemodel (= 4.2.8)
|
15
|
+
activesupport (= 4.2.8)
|
16
|
+
arel (~> 6.0)
|
17
|
+
activesupport (4.2.8)
|
18
|
+
i18n (~> 0.7)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
21
|
+
tzinfo (~> 1.1)
|
22
|
+
appraisal (2.1.0)
|
23
|
+
bundler
|
24
|
+
rake
|
25
|
+
thor (>= 0.14.0)
|
26
|
+
arel (6.0.4)
|
27
|
+
builder (3.2.3)
|
28
|
+
diff-lcs (1.3)
|
29
|
+
i18n (0.8.1)
|
30
|
+
minitest (5.10.1)
|
31
|
+
mysql2 (0.4.5)
|
32
|
+
rake (12.0.0)
|
33
|
+
rspec (3.5.0)
|
34
|
+
rspec-core (~> 3.5.0)
|
35
|
+
rspec-expectations (~> 3.5.0)
|
36
|
+
rspec-mocks (~> 3.5.0)
|
37
|
+
rspec-core (3.5.4)
|
38
|
+
rspec-support (~> 3.5.0)
|
39
|
+
rspec-expectations (3.5.0)
|
40
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
41
|
+
rspec-support (~> 3.5.0)
|
42
|
+
rspec-mocks (3.5.0)
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
44
|
+
rspec-support (~> 3.5.0)
|
45
|
+
rspec-support (3.5.0)
|
46
|
+
thor (0.19.4)
|
47
|
+
thread_safe (0.3.6)
|
48
|
+
tzinfo (1.2.2)
|
49
|
+
thread_safe (~> 0.1)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
ruby
|
53
|
+
|
54
|
+
DEPENDENCIES
|
55
|
+
activerecord (= 4.2.8)
|
56
|
+
appraisal
|
57
|
+
bundler (~> 1)
|
58
|
+
mysql2
|
59
|
+
rake
|
60
|
+
rspec
|
61
|
+
zdm!
|
62
|
+
|
63
|
+
BUNDLED WITH
|
64
|
+
1.14.5
|
@@ -0,0 +1,63 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
zdm (1.0.0)
|
5
|
+
activerecord (>= 4.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (5.0.1)
|
11
|
+
activesupport (= 5.0.1)
|
12
|
+
activerecord (5.0.1)
|
13
|
+
activemodel (= 5.0.1)
|
14
|
+
activesupport (= 5.0.1)
|
15
|
+
arel (~> 7.0)
|
16
|
+
activesupport (5.0.1)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (~> 0.7)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
tzinfo (~> 1.1)
|
21
|
+
appraisal (2.1.0)
|
22
|
+
bundler
|
23
|
+
rake
|
24
|
+
thor (>= 0.14.0)
|
25
|
+
arel (7.1.4)
|
26
|
+
concurrent-ruby (1.0.4)
|
27
|
+
diff-lcs (1.3)
|
28
|
+
i18n (0.8.1)
|
29
|
+
minitest (5.10.1)
|
30
|
+
mysql2 (0.4.5)
|
31
|
+
rake (12.0.0)
|
32
|
+
rspec (3.5.0)
|
33
|
+
rspec-core (~> 3.5.0)
|
34
|
+
rspec-expectations (~> 3.5.0)
|
35
|
+
rspec-mocks (~> 3.5.0)
|
36
|
+
rspec-core (3.5.4)
|
37
|
+
rspec-support (~> 3.5.0)
|
38
|
+
rspec-expectations (3.5.0)
|
39
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
40
|
+
rspec-support (~> 3.5.0)
|
41
|
+
rspec-mocks (3.5.0)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.5.0)
|
44
|
+
rspec-support (3.5.0)
|
45
|
+
thor (0.19.4)
|
46
|
+
thread_safe (0.3.6)
|
47
|
+
tzinfo (1.2.2)
|
48
|
+
thread_safe (~> 0.1)
|
49
|
+
|
50
|
+
PLATFORMS
|
51
|
+
ruby
|
52
|
+
|
53
|
+
DEPENDENCIES
|
54
|
+
activerecord (= 5.0.1)
|
55
|
+
appraisal
|
56
|
+
bundler (~> 1)
|
57
|
+
mysql2
|
58
|
+
rake
|
59
|
+
rspec
|
60
|
+
zdm!
|
61
|
+
|
62
|
+
BUNDLED WITH
|
63
|
+
1.14.5
|
data/lib/version.rb
ADDED
data/lib/zdm.rb
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
module Zdm
|
2
|
+
require 'version'
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_accessor :io
|
6
|
+
|
7
|
+
def change_table(name, &block)
|
8
|
+
table = Table.new(name)
|
9
|
+
yield table
|
10
|
+
Migrator.new(table).migrate!
|
11
|
+
cleanup if defined?(Rails) && Rails.env.development?
|
12
|
+
end
|
13
|
+
|
14
|
+
def cleanup(before: nil)
|
15
|
+
conn = ActiveRecord::Base.connection
|
16
|
+
zdm_tables = conn.send(tables_method).select { |name| name.starts_with?('zdm_') }
|
17
|
+
zdm_tables.each { |name| Migrator.new(Table.new(name.sub(/^zdm_/, ''))).cleanup }
|
18
|
+
|
19
|
+
zdm_archive_tables = conn.send(tables_method).select { |name| name.starts_with?('zdma_') }
|
20
|
+
if before
|
21
|
+
zdm_archive_tables.select! { |table|
|
22
|
+
Time.strptime(table, 'zdma_%Y%m%d_%H%M%S%N') <= before
|
23
|
+
}
|
24
|
+
end
|
25
|
+
zdm_archive_tables.each { |name| conn.execute('DROP TABLE `%s`' % name) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def tables_method
|
29
|
+
ActiveRecord.version.to_s =~ /^5/ ? :data_sources : :tables
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Table
|
34
|
+
attr_reader :origin, :copy, :archive, :statements
|
35
|
+
|
36
|
+
def initialize(name)
|
37
|
+
@origin = name
|
38
|
+
@copy = "zdm_#{name}"
|
39
|
+
@archive = "zdma_#{Time.now.strftime("%Y%m%d_%H%M%S%N")}_#{name}"[0..64]
|
40
|
+
@statements = []
|
41
|
+
end
|
42
|
+
|
43
|
+
def ddl(statement)
|
44
|
+
@statements << statement
|
45
|
+
end
|
46
|
+
|
47
|
+
def alter(definition)
|
48
|
+
ddl('ALTER TABLE `%s` %s' % [@copy, definition])
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_column(name, definition)
|
52
|
+
ddl('ALTER TABLE `%s` ADD COLUMN `%s` %s' % [@copy, name, definition])
|
53
|
+
end
|
54
|
+
|
55
|
+
def change_column(name, definition)
|
56
|
+
ddl('ALTER TABLE `%s` MODIFY COLUMN `%s` %s' % [@copy, name, definition])
|
57
|
+
end
|
58
|
+
|
59
|
+
def remove_column(name)
|
60
|
+
ddl('ALTER TABLE `%s` DROP `%s`' % [@copy, name])
|
61
|
+
end
|
62
|
+
|
63
|
+
def rename_column(old_name, new_name)
|
64
|
+
raise "Unsupported: you must first run a migration adding the column `#{new_name}`, deploy the code live, then run another migration at a later time to remove the column `#{old_name}`"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Migrator
|
69
|
+
attr_reader :table
|
70
|
+
|
71
|
+
def initialize(table)
|
72
|
+
@table = table
|
73
|
+
end
|
74
|
+
|
75
|
+
def migrate!
|
76
|
+
validate
|
77
|
+
set_session_lock_wait_timeouts
|
78
|
+
cleanup
|
79
|
+
create_destination_table
|
80
|
+
drop_destination_indexes
|
81
|
+
apply_ddl_statements
|
82
|
+
create_triggers
|
83
|
+
batched_copy
|
84
|
+
create_destination_indexes
|
85
|
+
atomic_switcharoo!
|
86
|
+
ensure
|
87
|
+
cleanup
|
88
|
+
end
|
89
|
+
|
90
|
+
def cleanup
|
91
|
+
drop_triggers
|
92
|
+
execute('DROP TABLE IF EXISTS `%s`' % table.copy)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def connection
|
98
|
+
ActiveRecord::Base.connection
|
99
|
+
end
|
100
|
+
|
101
|
+
def execute(stmt)
|
102
|
+
connection.execute(stmt)
|
103
|
+
end
|
104
|
+
|
105
|
+
def columns(table)
|
106
|
+
connection.columns(table).map(&:name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def common_columns
|
110
|
+
@common_columns ||= (columns(table.origin) & columns(table.copy))
|
111
|
+
end
|
112
|
+
|
113
|
+
def validate
|
114
|
+
unless connection.columns(table.origin).detect {|c| c.name == 'id'}&.extra == 'auto_increment'
|
115
|
+
raise 'Cannot migrate table `%s`, missing auto increment primary key `id`' % table.origin
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
LOCK_WAIT_TIMEOUT_DELTA = -2 # seconds
|
120
|
+
def set_session_lock_wait_timeouts
|
121
|
+
timeout = connection.select_one("SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout'")
|
122
|
+
if timeout
|
123
|
+
execute('SET SESSION innodb_lock_wait_timeout=%d' % (timeout['Value'].to_i + LOCK_WAIT_TIMEOUT_DELTA))
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def create_destination_table
|
128
|
+
execute('CREATE TABLE `%s` LIKE `%s`' % [table.copy, table.origin])
|
129
|
+
end
|
130
|
+
|
131
|
+
def apply_ddl_statements
|
132
|
+
table.statements.each { |statement| execute(statement) }
|
133
|
+
end
|
134
|
+
|
135
|
+
def atomic_switcharoo!
|
136
|
+
execute('RENAME TABLE `%s` to `%s`, `%s` to `%s`' % [table.origin, table.archive, table.copy, table.origin])
|
137
|
+
end
|
138
|
+
|
139
|
+
def create_triggers
|
140
|
+
create_delete_trigger
|
141
|
+
create_insert_trigger
|
142
|
+
create_update_trigger
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_delete_trigger
|
146
|
+
execute(<<-SQL.squish)
|
147
|
+
CREATE TRIGGER `#{trigger_name(:del)}`
|
148
|
+
AFTER DELETE ON `#{table.origin}` FOR EACH ROW
|
149
|
+
DELETE IGNORE FROM `#{table.copy}` WHERE `#{table.copy}`.`id` = `OLD`.`id`
|
150
|
+
SQL
|
151
|
+
end
|
152
|
+
|
153
|
+
def create_insert_trigger
|
154
|
+
execute(<<-SQL.squish)
|
155
|
+
CREATE TRIGGER `#{trigger_name(:ins)}`
|
156
|
+
AFTER INSERT ON `#{table.origin}` FOR EACH ROW
|
157
|
+
REPLACE INTO `#{table.copy}` SET #{trigger_column_setters}
|
158
|
+
SQL
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_update_trigger
|
162
|
+
execute(<<-SQL.squish)
|
163
|
+
CREATE TRIGGER `#{trigger_name(:upd)}`
|
164
|
+
AFTER UPDATE ON `#{table.origin}` FOR EACH ROW
|
165
|
+
REPLACE INTO `#{table.copy}` SET #{trigger_column_setters}
|
166
|
+
SQL
|
167
|
+
end
|
168
|
+
|
169
|
+
def trigger_column_setters
|
170
|
+
common_columns.map { |name| "`#{name}`=`NEW`.`#{name}`"}.join(', ')
|
171
|
+
end
|
172
|
+
|
173
|
+
def drop_triggers
|
174
|
+
execute('DROP TRIGGER IF EXISTS `%s`' % trigger_name(:del))
|
175
|
+
execute('DROP TRIGGER IF EXISTS `%s`' % trigger_name(:ins))
|
176
|
+
execute('DROP TRIGGER IF EXISTS `%s`' % trigger_name(:upd))
|
177
|
+
end
|
178
|
+
|
179
|
+
def trigger_name(trigger_type)
|
180
|
+
"zdmt_#{trigger_type}_#{table.origin}"[0...64]
|
181
|
+
end
|
182
|
+
|
183
|
+
# Drop indexes to speed up batched_copy
|
184
|
+
def drop_destination_indexes
|
185
|
+
@indexes = connection.indexes(table.copy).reject(&:unique)
|
186
|
+
@indexes.each do |index_def|
|
187
|
+
execute('ALTER TABLE `%s` DROP INDEX `%s`' % [table.copy, index_def.name])
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Recreate the indexes previously dropped
|
192
|
+
def create_destination_indexes
|
193
|
+
@indexes.each do |index_def|
|
194
|
+
opts = { name: index_def.name, using: index_def.using }
|
195
|
+
if index_def.lengths.compact.any?
|
196
|
+
opts[:length] = Hash[index_def.columns.map.with_index { |col, idx| [col, index_def.lengths[idx]] }]
|
197
|
+
end
|
198
|
+
connection.add_index(table.copy, index_def.columns, opts)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
BATCH_SIZE = 40_000
|
203
|
+
DECREASE_THROTTLER = 4 # seconds
|
204
|
+
DECREASE_SIZE = 5_000
|
205
|
+
MIN_BATCH_SIZE = 10_000
|
206
|
+
PROGRESS_EVERY = 30 # seconds
|
207
|
+
def batched_copy
|
208
|
+
min = connection.select_value('SELECT MIN(`id`) FROM %s' % table.origin)
|
209
|
+
return unless min
|
210
|
+
|
211
|
+
max = connection.select_value('SELECT MAX(`id`) FROM %s' % table.origin)
|
212
|
+
todo = max - min + 1
|
213
|
+
|
214
|
+
insert_columns = common_columns.map {|c| "`#{c}`"}.join(', ')
|
215
|
+
select_columns = common_columns.map {|c| "`#{table.origin}`.`#{c}`"}.join(', ')
|
216
|
+
|
217
|
+
batch_size = BATCH_SIZE
|
218
|
+
batch_end = min - 1
|
219
|
+
start_time = last_progress = Time.now
|
220
|
+
while true
|
221
|
+
batch_start = batch_end + 1
|
222
|
+
batch_end = [batch_start + batch_size - 1, max].min
|
223
|
+
start_batch_time = Time.now
|
224
|
+
|
225
|
+
execute(<<-SQL.squish)
|
226
|
+
INSERT IGNORE INTO `#{table.copy}` (#{insert_columns})
|
227
|
+
SELECT #{select_columns}
|
228
|
+
FROM `#{table.origin}`
|
229
|
+
WHERE `#{table.origin}`.`id` BETWEEN #{batch_start} AND #{batch_end}
|
230
|
+
SQL
|
231
|
+
|
232
|
+
if $exit
|
233
|
+
write('Received SIGTERM, exiting...')
|
234
|
+
cleanup
|
235
|
+
exit 1
|
236
|
+
end
|
237
|
+
|
238
|
+
# The end!
|
239
|
+
break if batch_end >= max
|
240
|
+
|
241
|
+
# Throttle
|
242
|
+
current_time = Time.now
|
243
|
+
if (current_time - start_batch_time) > DECREASE_THROTTLER
|
244
|
+
batch_size = [(batch_size - DECREASE_SIZE).to_i, MIN_BATCH_SIZE].max
|
245
|
+
end
|
246
|
+
|
247
|
+
# Periodically show progress
|
248
|
+
if (current_time - last_progress) >= PROGRESS_EVERY
|
249
|
+
last_progress = current_time
|
250
|
+
done = batch_end - min + 1
|
251
|
+
write("%.2f%% (#{done}/#{todo})" % (done.to_f / todo * 100.0))
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
duration = Time.now - start_time
|
256
|
+
duration = (duration < 2*60) ? "#{duration.to_i} secs" : "#{(duration / 60).to_i} mins"
|
257
|
+
write("Completed (#{duration})")
|
258
|
+
end
|
259
|
+
|
260
|
+
def write(msg)
|
261
|
+
return if Zdm.io == false
|
262
|
+
io = Zdm.io || $stderr
|
263
|
+
io.puts("#{table.origin}: #{msg}")
|
264
|
+
io.flush
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
trap('TERM') { $exit = true }
|
data/spec/database.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
test:
|
2
|
+
adapter: mysql2
|
3
|
+
encoding: utf8mb4
|
4
|
+
charset: utf8mb4
|
5
|
+
collation: utf8mb4_unicode_ci
|
6
|
+
host: <%= ENV['DB_HOST'] %>
|
7
|
+
port: <%= ENV['DB_PORT'] %>
|
8
|
+
username: <%= ENV['DB_USERNAME'] || 'travis' %>
|
9
|
+
password: <%= ENV['DB_PASSWORD'] %>
|
10
|
+
database: <%= ENV['DB_DATABASE'] || 'zdm_test' %>
|
11
|
+
sslkey: <%= ENV['DB_SSLKEY'] %>
|
12
|
+
sslcert: <%= ENV['DB_SSLCERT'] %>
|
13
|
+
sslca: <%= ENV['DB_SSLCA'] %>
|
14
|
+
sslcapath: <%= ENV['DB_SSLCAPATH'] %>
|
15
|
+
sslcipher: <%= ENV['DB_SSLCIPHER'] %>
|
16
|
+
sslverify: <%= ENV['DB_SSLVERIFY'] || false %>
|
17
|
+
strict: false
|
18
|
+
variables:
|
19
|
+
sql_mode: 'NO_ENGINE_SUBSTITUTION'
|
20
|
+
character_set_connection: utf8mb4
|
21
|
+
collation_connection: utf8mb4_unicode_ci
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
3
|
+
|
4
|
+
$stderr.puts("Running Specs using Ruby v#{RUBY_VERSION}")
|
5
|
+
|
6
|
+
require 'rspec'
|
7
|
+
require 'logger'
|
8
|
+
require 'zdm'
|
9
|
+
require 'active_record'
|
10
|
+
require 'yaml'
|
11
|
+
require 'erb'
|
12
|
+
|
13
|
+
# require 'rspec/support'
|
14
|
+
# RSpec::Support.require_rspec_support "object_formatter"
|
15
|
+
# RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = nil
|
16
|
+
|
17
|
+
config = YAML::load(ERB.new(IO.read(File.dirname(__FILE__) + '/database.yml')).result)
|
18
|
+
ActiveRecord::Base.establish_connection(config['test'])
|
19
|
+
|
20
|
+
ActiveRecord::Schema.define version: 0 do
|
21
|
+
create_table :people, force: true do |t|
|
22
|
+
t.integer :account_id
|
23
|
+
t.string :name, limit: 30
|
24
|
+
t.string :code
|
25
|
+
t.datetime :created_at
|
26
|
+
end
|
27
|
+
add_index(:people, :name, unique: true)
|
28
|
+
add_index(:people, [:account_id, :code], length: {account_id: nil, code: 191})
|
29
|
+
|
30
|
+
create_table :people_teams, id: false, force: true do |t|
|
31
|
+
t.integer :team_id, null: false
|
32
|
+
t.integer :person_id, null: false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
ActiveRecord::Base.connection.execute(%[INSERT INTO people(account_id, name, code, created_at) VALUES (10,'foo','bar','2017-03-01 23:59:59')])
|
37
|
+
ActiveRecord::Base.connection.execute(%[INSERT INTO people(account_id, name, code, created_at) VALUES (20,'foo2','bar2','2017-03-02 23:59:59')])
|
38
|
+
|
data/spec/zdm_spec.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zdm do
|
4
|
+
|
5
|
+
before(:example) {
|
6
|
+
Zdm.io = false
|
7
|
+
Zdm.cleanup
|
8
|
+
}
|
9
|
+
|
10
|
+
it 'requires an autoincrement primary key `id` field' do
|
11
|
+
expect{Zdm.change_table(:people_teams) {}}.to raise_error('Cannot migrate table `people_teams`, missing auto increment primary key `id`')
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'sends output to stderr' do
|
15
|
+
Zdm.io = nil
|
16
|
+
filename = "test_stderr.#{$$}.log"
|
17
|
+
at_exit { File.unlink(filename) rescue nil }
|
18
|
+
orig_err = STDERR.dup
|
19
|
+
STDERR.reopen(filename, 'a')
|
20
|
+
STDERR.sync = true
|
21
|
+
begin
|
22
|
+
Zdm.change_table(:people) {}
|
23
|
+
expect(File.read(filename).strip).to eq('people: Completed (0 secs)')
|
24
|
+
ensure
|
25
|
+
STDERR.reopen(orig_err)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'migrates live tables' do
|
30
|
+
Zdm.change_table(:people) do |m|
|
31
|
+
m.alter("DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci")
|
32
|
+
m.add_column('test', "varchar(32) DEFAULT 'foo'")
|
33
|
+
m.change_column('name', 'varchar(99) NOT NULL')
|
34
|
+
end
|
35
|
+
|
36
|
+
conn = ActiveRecord::Base.connection
|
37
|
+
stmt = conn.select_rows('show create table people')[0][1]
|
38
|
+
expect(stmt.squish).to eq(<<-EOS.squish)
|
39
|
+
CREATE TABLE `people` (
|
40
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
41
|
+
`account_id` int(11) DEFAULT NULL,
|
42
|
+
`name` varchar(99) COLLATE utf8_unicode_ci NOT NULL,
|
43
|
+
`code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
44
|
+
`created_at` datetime DEFAULT NULL,
|
45
|
+
`test` varchar(32) COLLATE utf8_unicode_ci DEFAULT 'foo',
|
46
|
+
PRIMARY KEY (`id`), UNIQUE KEY `index_people_on_name` (`name`),
|
47
|
+
KEY `index_people_on_account_id_and_code` (`account_id`,`code`(191)) USING BTREE
|
48
|
+
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
|
49
|
+
EOS
|
50
|
+
|
51
|
+
archive_tables = conn.send(Zdm.tables_method).select { |name| name.starts_with?('zdma_') }
|
52
|
+
expect(archive_tables.length).to eq(1)
|
53
|
+
rows = conn.select_rows("SELECT * FROM #{archive_tables[0]}")
|
54
|
+
expect(rows).to eq([
|
55
|
+
[1, 10, 'foo', 'bar', '2017-03-01 23:59:59 UTC'],
|
56
|
+
[2, 20, 'foo2', 'bar2', '2017-03-02 23:59:59 UTC']
|
57
|
+
])
|
58
|
+
|
59
|
+
rows = conn.select_rows("SELECT * FROM `people`")
|
60
|
+
expect(rows).to eq([
|
61
|
+
[1, 10, 'foo', 'bar', '2017-03-01 23:59:59 UTC', 'foo'],
|
62
|
+
[2, 20, 'foo2', 'bar2', '2017-03-02 23:59:59 UTC', 'foo']
|
63
|
+
])
|
64
|
+
|
65
|
+
Zdm.change_table(:people) do |m|
|
66
|
+
m.alter("DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
67
|
+
m.remove_column('test')
|
68
|
+
m.change_column('name', 'varchar(30)')
|
69
|
+
end
|
70
|
+
|
71
|
+
stmt = conn.select_rows('show create table people')[0][1]
|
72
|
+
expect(stmt.squish).to eq(<<-EOS.squish)
|
73
|
+
CREATE TABLE `people` (
|
74
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
75
|
+
`account_id` int(11) DEFAULT NULL,
|
76
|
+
`name` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
77
|
+
`code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
78
|
+
`created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`),
|
79
|
+
UNIQUE KEY `index_people_on_name` (`name`),
|
80
|
+
KEY `index_people_on_account_id_and_code` (`account_id`,`code`(191)) USING BTREE
|
81
|
+
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
82
|
+
EOS
|
83
|
+
|
84
|
+
archive_tables = conn.send(Zdm.tables_method).select { |name| name.starts_with?('zdma_') }
|
85
|
+
expect(archive_tables.length).to eq(2)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
data/zdm.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'zdm'
|
6
|
+
s.version = Zdm::VERSION
|
7
|
+
s.authors = ['ITRP Institute, Inc.']
|
8
|
+
s.email = ['support@itrp.com']
|
9
|
+
s.description = %q{Zero Downtime Migrator of mysql compatible databases}
|
10
|
+
s.summary = %q{Zero Downtime Migrator for mysql in ruby}
|
11
|
+
s.homepage = 'https://github.com/itrp/zdm'
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split($/)
|
15
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
16
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
17
|
+
s.require_paths = ['lib']
|
18
|
+
|
19
|
+
s.add_dependency 'activerecord', '>= 4.0'
|
20
|
+
|
21
|
+
s.add_development_dependency 'bundler', '~> 1'
|
22
|
+
s.add_development_dependency 'rake'
|
23
|
+
s.add_development_dependency 'rspec'
|
24
|
+
s.add_development_dependency 'mysql2'
|
25
|
+
s.add_development_dependency 'appraisal'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zdm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ITRP Institute, Inc.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mysql2
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: appraisal
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Zero Downtime Migrator of mysql compatible databases
|
98
|
+
email:
|
99
|
+
- support@itrp.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".travis.yml"
|
106
|
+
- Appraisals
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- gemfiles/4.1.gemfile
|
112
|
+
- gemfiles/4.1.gemfile.lock
|
113
|
+
- gemfiles/4.2.gemfile
|
114
|
+
- gemfiles/4.2.gemfile.lock
|
115
|
+
- gemfiles/5.0.gemfile
|
116
|
+
- gemfiles/5.0.gemfile.lock
|
117
|
+
- lib/version.rb
|
118
|
+
- lib/zdm.rb
|
119
|
+
- spec/database.yml
|
120
|
+
- spec/spec_helper.rb
|
121
|
+
- spec/zdm_spec.rb
|
122
|
+
- zdm.gemspec
|
123
|
+
homepage: https://github.com/itrp/zdm
|
124
|
+
licenses:
|
125
|
+
- MIT
|
126
|
+
metadata: {}
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 2.6.8
|
144
|
+
signing_key:
|
145
|
+
specification_version: 4
|
146
|
+
summary: Zero Downtime Migrator for mysql in ruby
|
147
|
+
test_files:
|
148
|
+
- spec/database.yml
|
149
|
+
- spec/spec_helper.rb
|
150
|
+
- spec/zdm_spec.rb
|