zdm 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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'
@@ -0,0 +1,9 @@
1
+ appraise '4.1' do
2
+ gem 'activerecord', '4.1.15'
3
+ end
4
+ appraise '4.2' do
5
+ gem 'activerecord', '4.2.8'
6
+ end
7
+ appraise '5.0' do
8
+ gem 'activerecord', '5.0.1'
9
+ end
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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.
@@ -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
+ [![Build Status](https://travis-ci.org/itrp/zdm.png)](https://travis-ci.org/itrp/zdm)
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'bundler/gem_tasks'
5
+
6
+ require 'rspec/core/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ desc 'Run the specs.'
11
+ task default: :spec
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "4.1.15"
6
+
7
+ gemspec :path => "../"
@@ -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,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "4.2.8"
6
+
7
+ gemspec :path => "../"
@@ -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,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "5.0.1"
6
+
7
+ gemspec :path => "../"
@@ -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
@@ -0,0 +1,3 @@
1
+ module Zdm
2
+ VERSION = '1.0.0'
3
+ end
@@ -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 }
@@ -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
@@ -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
+
@@ -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
@@ -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