lhm-teak 3.6.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/.github/workflows/test.yml +43 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +183 -0
- data/.travis.yml +21 -0
- data/Appraisals +24 -0
- data/CHANGELOG.md +254 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +27 -0
- data/README.md +335 -0
- data/Rakefile +33 -0
- data/dev.yml +45 -0
- data/docker-compose.yml +60 -0
- data/gemfiles/activerecord_5.2.gemfile +9 -0
- data/gemfiles/activerecord_5.2.gemfile.lock +66 -0
- data/gemfiles/activerecord_6.0.gemfile +7 -0
- data/gemfiles/activerecord_6.0.gemfile.lock +68 -0
- data/gemfiles/activerecord_6.1.gemfile +7 -0
- data/gemfiles/activerecord_6.1.gemfile.lock +67 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +65 -0
- data/lhm.gemspec +38 -0
- data/lib/lhm/atomic_switcher.rb +46 -0
- data/lib/lhm/chunk_finder.rb +62 -0
- data/lib/lhm/chunk_insert.rb +61 -0
- data/lib/lhm/chunker.rb +95 -0
- data/lib/lhm/cleanup/current.rb +71 -0
- data/lib/lhm/command.rb +48 -0
- data/lib/lhm/connection.rb +108 -0
- data/lib/lhm/entangler.rb +112 -0
- data/lib/lhm/intersection.rb +51 -0
- data/lib/lhm/invoker.rb +100 -0
- data/lib/lhm/locked_switcher.rb +76 -0
- data/lib/lhm/migration.rb +51 -0
- data/lib/lhm/migrator.rb +244 -0
- data/lib/lhm/printer.rb +63 -0
- data/lib/lhm/proxysql_helper.rb +10 -0
- data/lib/lhm/railtie.rb +9 -0
- data/lib/lhm/sql_helper.rb +77 -0
- data/lib/lhm/sql_retry.rb +180 -0
- data/lib/lhm/table.rb +121 -0
- data/lib/lhm/table_name.rb +23 -0
- data/lib/lhm/test_support.rb +35 -0
- data/lib/lhm/throttler/slave_lag.rb +162 -0
- data/lib/lhm/throttler/threads_running.rb +53 -0
- data/lib/lhm/throttler/time.rb +29 -0
- data/lib/lhm/throttler.rb +36 -0
- data/lib/lhm/timestamp.rb +11 -0
- data/lib/lhm/version.rb +6 -0
- data/lib/lhm-shopify.rb +1 -0
- data/lib/lhm.rb +156 -0
- data/scripts/helpers/wait-for-dbs.sh +21 -0
- data/scripts/mysql/reader/create_replication.sql +10 -0
- data/scripts/mysql/writer/create_test_db.sql +1 -0
- data/scripts/mysql/writer/create_users.sql +6 -0
- data/scripts/proxysql/proxysql.cnf +117 -0
- data/shipit.rubygems.yml +0 -0
- data/spec/.lhm.example +4 -0
- data/spec/README.md +58 -0
- data/spec/fixtures/bigint_table.ddl +4 -0
- data/spec/fixtures/composite_primary_key.ddl +6 -0
- data/spec/fixtures/composite_primary_key_dest.ddl +6 -0
- data/spec/fixtures/custom_primary_key.ddl +6 -0
- data/spec/fixtures/custom_primary_key_dest.ddl +6 -0
- data/spec/fixtures/destination.ddl +6 -0
- data/spec/fixtures/lines.ddl +7 -0
- data/spec/fixtures/origin.ddl +6 -0
- data/spec/fixtures/permissions.ddl +5 -0
- data/spec/fixtures/small_table.ddl +4 -0
- data/spec/fixtures/tracks.ddl +5 -0
- data/spec/fixtures/users.ddl +14 -0
- data/spec/fixtures/wo_id_int_column.ddl +6 -0
- data/spec/integration/atomic_switcher_spec.rb +129 -0
- data/spec/integration/chunk_insert_spec.rb +30 -0
- data/spec/integration/chunker_spec.rb +269 -0
- data/spec/integration/cleanup_spec.rb +147 -0
- data/spec/integration/database.yml +25 -0
- data/spec/integration/entangler_spec.rb +68 -0
- data/spec/integration/integration_helper.rb +252 -0
- data/spec/integration/invoker_spec.rb +33 -0
- data/spec/integration/lhm_spec.rb +659 -0
- data/spec/integration/lock_wait_timeout_spec.rb +30 -0
- data/spec/integration/locked_switcher_spec.rb +50 -0
- data/spec/integration/proxysql_spec.rb +34 -0
- data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
- data/spec/integration/sql_retry/lock_wait_spec.rb +127 -0
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +114 -0
- data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
- data/spec/integration/table_spec.rb +83 -0
- data/spec/integration/toxiproxy_helper.rb +40 -0
- data/spec/test_helper.rb +69 -0
- data/spec/unit/atomic_switcher_spec.rb +29 -0
- data/spec/unit/chunk_finder_spec.rb +73 -0
- data/spec/unit/chunk_insert_spec.rb +67 -0
- data/spec/unit/chunker_spec.rb +176 -0
- data/spec/unit/connection_spec.rb +111 -0
- data/spec/unit/entangler_spec.rb +187 -0
- data/spec/unit/intersection_spec.rb +51 -0
- data/spec/unit/lhm_spec.rb +46 -0
- data/spec/unit/locked_switcher_spec.rb +46 -0
- data/spec/unit/migrator_spec.rb +144 -0
- data/spec/unit/printer_spec.rb +85 -0
- data/spec/unit/sql_helper_spec.rb +28 -0
- data/spec/unit/table_name_spec.rb +39 -0
- data/spec/unit/table_spec.rb +47 -0
- data/spec/unit/throttler/slave_lag_spec.rb +322 -0
- data/spec/unit/throttler/threads_running_spec.rb +64 -0
- data/spec/unit/throttler_spec.rb +124 -0
- data/spec/unit/unit_helper.rb +26 -0
- metadata +366 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/table'
|
|
7
|
+
require 'lhm/migration'
|
|
8
|
+
require 'lhm/entangler'
|
|
9
|
+
require 'lhm/connection'
|
|
10
|
+
|
|
11
|
+
describe Lhm::Entangler do
|
|
12
|
+
include UnitHelper
|
|
13
|
+
|
|
14
|
+
before(:each) do
|
|
15
|
+
@origin = Lhm::Table.new('origin')
|
|
16
|
+
@destination = Lhm::Table.new('destination')
|
|
17
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
|
18
|
+
@entangler = Lhm::Entangler.new(@migration)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe 'activation' do
|
|
22
|
+
before(:each) do
|
|
23
|
+
@origin.columns['info'] = { :type => 'varchar(255)' }
|
|
24
|
+
@origin.columns['tags'] = { :type => 'varchar(255)' }
|
|
25
|
+
|
|
26
|
+
@destination.columns['info'] = { :type => 'varchar(255)' }
|
|
27
|
+
@destination.columns['tags'] = { :type => 'varchar(255)' }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'should create insert trigger to destination table' do
|
|
31
|
+
ddl = %Q{
|
|
32
|
+
create trigger `lhmt_ins_origin`
|
|
33
|
+
after insert on `origin` for each row
|
|
34
|
+
replace into `destination` (`info`, `tags`) /* large hadron migration */
|
|
35
|
+
values (`NEW`.`info`, `NEW`.`tags`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
value(@entangler.entangle).must_include strip(ddl)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'should create an update trigger to the destination table' do
|
|
42
|
+
ddl = %Q{
|
|
43
|
+
create trigger `lhmt_upd_origin`
|
|
44
|
+
after update on `origin` for each row
|
|
45
|
+
replace into `destination` (`info`, `tags`) /* large hadron migration */
|
|
46
|
+
values (`NEW`.`info`, `NEW`.`tags`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
value(@entangler.entangle).must_include strip(ddl)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'should create a delete trigger to the destination table' do
|
|
53
|
+
ddl = %Q{
|
|
54
|
+
create trigger `lhmt_del_origin`
|
|
55
|
+
after delete on `origin` for each row
|
|
56
|
+
delete ignore from `destination` /* large hadron migration */
|
|
57
|
+
where `destination`.`id` = OLD.`id`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
value(@entangler.entangle).must_include strip(ddl)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'should retry trigger creation when it hits a lock wait timeout' do
|
|
64
|
+
tries = 1
|
|
65
|
+
ar_connection = mock()
|
|
66
|
+
ar_connection.stubs(:execute)
|
|
67
|
+
.returns([["dummy"]], [["dummy"]], [["dummy"]])
|
|
68
|
+
.then
|
|
69
|
+
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
|
70
|
+
ar_connection.stubs(:active?).returns(true)
|
|
71
|
+
|
|
72
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
73
|
+
reconnect_with_consistent_host: true,
|
|
74
|
+
retriable: {
|
|
75
|
+
base_interval: 0,
|
|
76
|
+
tries: tries
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
@entangler = Lhm::Entangler.new(@migration, connection)
|
|
81
|
+
|
|
82
|
+
assert_raises(Mysql2::Error) { @entangler.before }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'should not retry trigger creation with other mysql errors' do
|
|
86
|
+
ar_connection = mock()
|
|
87
|
+
ar_connection.stubs(:execute)
|
|
88
|
+
.returns([["dummy"]], [["dummy"]], [["dummy"]])
|
|
89
|
+
.then
|
|
90
|
+
.raises(Mysql2::Error, 'The MySQL server is running with the --read-only option so it cannot execute this statement.')
|
|
91
|
+
ar_connection.stubs(:active?).returns(true)
|
|
92
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
93
|
+
reconnect_with_consistent_host: true,
|
|
94
|
+
retriable: {
|
|
95
|
+
base_interval: 0
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
@entangler = Lhm::Entangler.new(@migration, connection)
|
|
100
|
+
assert_raises(Mysql2::Error) { @entangler.before }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'should succesfully finish after retrying' do
|
|
104
|
+
ar_connection = mock()
|
|
105
|
+
ar_connection.stubs(:execute)
|
|
106
|
+
.returns([["dummy"]], [["dummy"]], [["dummy"]])
|
|
107
|
+
.then
|
|
108
|
+
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
|
109
|
+
.then
|
|
110
|
+
.returns([["dummy"]])
|
|
111
|
+
ar_connection.stubs(:active?).returns(true)
|
|
112
|
+
|
|
113
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
114
|
+
reconnect_with_consistent_host: true,
|
|
115
|
+
retriable: {
|
|
116
|
+
base_interval: 0
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
@entangler = Lhm::Entangler.new(@migration, connection)
|
|
121
|
+
|
|
122
|
+
assert @entangler.before
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'should retry as many times as specified by configuration' do
|
|
126
|
+
ar_connection = mock()
|
|
127
|
+
ar_connection.stubs(:execute)
|
|
128
|
+
.returns([["dummy"]], [["dummy"]], [["dummy"]]) # initial
|
|
129
|
+
.then
|
|
130
|
+
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
|
131
|
+
.then
|
|
132
|
+
.returns([["dummy"]]) # reconnect 1
|
|
133
|
+
.then
|
|
134
|
+
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
|
135
|
+
.then
|
|
136
|
+
.returns([["dummy"]]) # reconnect 2
|
|
137
|
+
.then
|
|
138
|
+
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
|
139
|
+
.then
|
|
140
|
+
.returns([["dummy"]]) # reconnect 3
|
|
141
|
+
.then
|
|
142
|
+
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction') # final error
|
|
143
|
+
ar_connection.stubs(:active?).returns(true)
|
|
144
|
+
|
|
145
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
146
|
+
reconnect_with_consistent_host: true,
|
|
147
|
+
retriable: {
|
|
148
|
+
tries: 5,
|
|
149
|
+
base_interval: 0
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
@entangler = Lhm::Entangler.new(@migration, connection)
|
|
154
|
+
|
|
155
|
+
assert_raises(Mysql2::Error) { @entangler.before }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe 'super long table names' do
|
|
159
|
+
before(:each) do
|
|
160
|
+
@origin = Lhm::Table.new('a' * 64)
|
|
161
|
+
@destination = Lhm::Table.new('b' * 64)
|
|
162
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
|
163
|
+
@entangler = Lhm::Entangler.new(@migration)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'should use truncated names' do
|
|
167
|
+
value(@entangler.trigger(:ins).length).must_be :<=, 64
|
|
168
|
+
value(@entangler.trigger(:upd).length).must_be :<=, 64
|
|
169
|
+
value(@entangler.trigger(:del).length).must_be :<=, 64
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe 'removal' do
|
|
175
|
+
it 'should remove insert trigger' do
|
|
176
|
+
value(@entangler.untangle).must_include('drop trigger if exists `lhmt_ins_origin`')
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'should remove update trigger' do
|
|
180
|
+
value(@entangler.untangle).must_include('drop trigger if exists `lhmt_upd_origin`')
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'should remove delete trigger' do
|
|
184
|
+
value(@entangler.untangle).must_include('drop trigger if exists `lhmt_del_origin`')
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/table'
|
|
7
|
+
require 'lhm/migrator'
|
|
8
|
+
|
|
9
|
+
describe Lhm::Intersection do
|
|
10
|
+
include UnitHelper
|
|
11
|
+
|
|
12
|
+
it 'should not have dropped changes' do
|
|
13
|
+
origin = Lhm::Table.new('origin')
|
|
14
|
+
origin.columns['dropped'] = varchar
|
|
15
|
+
origin.columns['retained'] = varchar
|
|
16
|
+
|
|
17
|
+
destination = Lhm::Table.new('destination')
|
|
18
|
+
destination.columns['retained'] = varchar
|
|
19
|
+
|
|
20
|
+
intersection = Lhm::Intersection.new(origin, destination)
|
|
21
|
+
value(intersection.destination.include?('dropped')).must_equal(false)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'should have unchanged columns' do
|
|
25
|
+
origin = Lhm::Table.new('origin')
|
|
26
|
+
origin.columns['dropped'] = varchar
|
|
27
|
+
origin.columns['retained'] = varchar
|
|
28
|
+
|
|
29
|
+
destination = Lhm::Table.new('destination')
|
|
30
|
+
destination.columns['retained'] = varchar
|
|
31
|
+
|
|
32
|
+
intersection = Lhm::Intersection.new(origin, destination)
|
|
33
|
+
value(intersection.destination).must_equal(['retained'])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'should have renamed columns' do
|
|
37
|
+
origin = Lhm::Table.new('origin')
|
|
38
|
+
origin.columns['old_name'] = varchar
|
|
39
|
+
|
|
40
|
+
destination = Lhm::Table.new('destination')
|
|
41
|
+
destination.columns['new_name'] = varchar
|
|
42
|
+
|
|
43
|
+
intersection = Lhm::Intersection.new(origin, destination, { 'old_name' => 'new_name' })
|
|
44
|
+
value(intersection.origin).must_equal(['old_name'])
|
|
45
|
+
value(intersection.destination).must_equal(['new_name'])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def varchar
|
|
49
|
+
{ :metadata => 'VARCHAR(255)' }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd.
|
|
2
|
+
|
|
3
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
4
|
+
|
|
5
|
+
describe Lhm do
|
|
6
|
+
|
|
7
|
+
before(:each) do
|
|
8
|
+
Lhm.remove_class_variable :@@logger if Lhm.class_variable_defined? :@@logger
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe 'logger' do
|
|
12
|
+
|
|
13
|
+
it 'should use the default parameters if no logger explicitly set' do
|
|
14
|
+
value(Lhm.logger).must_be_kind_of Logger
|
|
15
|
+
value(Lhm.logger.level).must_equal Logger::INFO
|
|
16
|
+
value(Lhm.logger.instance_eval { @logdev }.dev).must_equal STDOUT
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'should use s new logger if set' do
|
|
20
|
+
l = Logger.new('omg.ponies')
|
|
21
|
+
l.level = Logger::ERROR
|
|
22
|
+
Lhm.logger = l
|
|
23
|
+
|
|
24
|
+
value(Lhm.logger.level).must_equal Logger::ERROR
|
|
25
|
+
value(Lhm.logger.instance_eval { @logdev }.dev).must_be_kind_of File
|
|
26
|
+
value(Lhm.logger.instance_eval { @logdev }.dev.path).must_equal 'omg.ponies'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe 'api' do
|
|
31
|
+
|
|
32
|
+
before(:each) do
|
|
33
|
+
@connection = mock()
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'should create a new connection when calling setup' do
|
|
37
|
+
Lhm.setup(@connection)
|
|
38
|
+
value(Lhm.connection).must_be_kind_of(Lhm::Connection)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'should create a new connection when none is created' do
|
|
42
|
+
ActiveRecord::Base.stubs(:connection).returns(@connection)
|
|
43
|
+
value(Lhm.connection).must_be_kind_of(Lhm::Connection)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/table'
|
|
7
|
+
require 'lhm/migration'
|
|
8
|
+
require 'lhm/locked_switcher'
|
|
9
|
+
|
|
10
|
+
describe Lhm::LockedSwitcher do
|
|
11
|
+
include UnitHelper
|
|
12
|
+
|
|
13
|
+
before(:each) do
|
|
14
|
+
@start = Time.now
|
|
15
|
+
@origin = Lhm::Table.new('origin')
|
|
16
|
+
@destination = Lhm::Table.new('destination')
|
|
17
|
+
@migration = Lhm::Migration.new(@origin, @destination, @start)
|
|
18
|
+
@switcher = Lhm::LockedSwitcher.new(@migration, nil)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe 'uncommitted' do
|
|
22
|
+
it 'should disable autocommit first' do
|
|
23
|
+
value(@switcher.statements[0..1]).must_equal([
|
|
24
|
+
'set @lhm_auto_commit = @@session.autocommit',
|
|
25
|
+
'set session autocommit = 0'
|
|
26
|
+
])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'should reapply original autocommit settings at the end' do
|
|
30
|
+
value(@switcher.statements[-1])
|
|
31
|
+
.must_equal('set session autocommit = @lhm_auto_commit')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe 'switch' do
|
|
36
|
+
it 'should lock origin and destination table, switch, commit and unlock' do
|
|
37
|
+
value(@switcher.switch).must_equal([
|
|
38
|
+
'lock table `origin` write, `destination` write',
|
|
39
|
+
"alter table `origin` rename `#{ @migration.archive_name }`",
|
|
40
|
+
'alter table `destination` rename `origin`',
|
|
41
|
+
'commit',
|
|
42
|
+
'unlock tables'
|
|
43
|
+
])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/table'
|
|
7
|
+
require 'lhm/migrator'
|
|
8
|
+
|
|
9
|
+
describe Lhm::Migrator do
|
|
10
|
+
include UnitHelper
|
|
11
|
+
|
|
12
|
+
before(:each) do
|
|
13
|
+
@table = Lhm::Table.new('alt')
|
|
14
|
+
@creator = Lhm::Migrator.new(@table)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe 'index changes' do
|
|
18
|
+
it 'should add an index' do
|
|
19
|
+
@creator.add_index(:a)
|
|
20
|
+
|
|
21
|
+
value(@creator.statements).must_equal([
|
|
22
|
+
'create index `index_alt_on_a` on `lhmn_alt` (`a`)'
|
|
23
|
+
])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'should add a composite index' do
|
|
27
|
+
@creator.add_index([:a, :b])
|
|
28
|
+
|
|
29
|
+
value(@creator.statements).must_equal([
|
|
30
|
+
'create index `index_alt_on_a_and_b` on `lhmn_alt` (`a`, `b`)'
|
|
31
|
+
])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'should add an index with prefix length' do
|
|
35
|
+
@creator.add_index(['a(10)', 'b'])
|
|
36
|
+
|
|
37
|
+
value(@creator.statements).must_equal([
|
|
38
|
+
'create index `index_alt_on_a_and_b` on `lhmn_alt` (`a`(10), `b`)'
|
|
39
|
+
])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'should add an index with a custom name' do
|
|
43
|
+
@creator.add_index([:a, :b], :custom_index_name)
|
|
44
|
+
|
|
45
|
+
value(@creator.statements).must_equal([
|
|
46
|
+
'create index `custom_index_name` on `lhmn_alt` (`a`, `b`)'
|
|
47
|
+
])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'should raise an error when the index name is not a string or symbol' do
|
|
51
|
+
assert_raises ArgumentError do
|
|
52
|
+
@creator.add_index([:a, :b], :name => :custom_index_name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'should add a unique index' do
|
|
57
|
+
@creator.add_unique_index(['a(5)', :b])
|
|
58
|
+
|
|
59
|
+
value(@creator.statements).must_equal([
|
|
60
|
+
'create unique index `index_alt_on_a_and_b` on `lhmn_alt` (`a`(5), `b`)'
|
|
61
|
+
])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'should add a unique index with a custom name' do
|
|
65
|
+
@creator.add_unique_index([:a, :b], :custom_index_name)
|
|
66
|
+
|
|
67
|
+
value(@creator.statements).must_equal([
|
|
68
|
+
'create unique index `custom_index_name` on `lhmn_alt` (`a`, `b`)'
|
|
69
|
+
])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'should raise an error when the unique index name is not a string or symbol' do
|
|
73
|
+
assert_raises ArgumentError do
|
|
74
|
+
@creator.add_unique_index([:a, :b], :name => :custom_index_name)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'should remove an index' do
|
|
79
|
+
@creator.remove_index(['b', 'a'])
|
|
80
|
+
|
|
81
|
+
value(@creator.statements).must_equal([
|
|
82
|
+
'drop index `index_alt_on_b_and_a` on `lhmn_alt`'
|
|
83
|
+
])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'should remove an index with a custom name' do
|
|
87
|
+
@creator.remove_index([:a, :b], :custom_index_name)
|
|
88
|
+
|
|
89
|
+
value(@creator.statements).must_equal([
|
|
90
|
+
'drop index `custom_index_name` on `lhmn_alt`'
|
|
91
|
+
])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe 'column changes' do
|
|
96
|
+
it 'should add a column' do
|
|
97
|
+
@creator.add_column('logins', 'INT(12)')
|
|
98
|
+
|
|
99
|
+
value(@creator.statements).must_equal([
|
|
100
|
+
'alter table `lhmn_alt` add column `logins` INT(12)'
|
|
101
|
+
])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'should remove a column' do
|
|
105
|
+
@creator.remove_column('logins')
|
|
106
|
+
|
|
107
|
+
value(@creator.statements).must_equal([
|
|
108
|
+
'alter table `lhmn_alt` drop `logins`'
|
|
109
|
+
])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'should change a column' do
|
|
113
|
+
@creator.change_column('logins', 'INT(11)')
|
|
114
|
+
|
|
115
|
+
value(@creator.statements).must_equal([
|
|
116
|
+
'alter table `lhmn_alt` modify column `logins` INT(11)'
|
|
117
|
+
])
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe 'direct changes' do
|
|
122
|
+
it 'should accept a ddl statement' do
|
|
123
|
+
@creator.ddl('alter table `%s` add column `f` tinyint(1)' % @creator.name)
|
|
124
|
+
|
|
125
|
+
value(@creator.statements).must_equal([
|
|
126
|
+
'alter table `lhmn_alt` add column `f` tinyint(1)'
|
|
127
|
+
])
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
describe 'multiple changes' do
|
|
132
|
+
it 'should add two columns' do
|
|
133
|
+
@creator.add_column('first', 'VARCHAR(64)')
|
|
134
|
+
@creator.add_column('last', 'VARCHAR(64)')
|
|
135
|
+
value(@creator.statements.length).must_equal(2)
|
|
136
|
+
|
|
137
|
+
value(@creator.statements[0])
|
|
138
|
+
.must_equal('alter table `lhmn_alt` add column `first` VARCHAR(64)')
|
|
139
|
+
|
|
140
|
+
value(@creator.statements[1])
|
|
141
|
+
.must_equal('alter table `lhmn_alt` add column `last` VARCHAR(64)')
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
2
|
+
|
|
3
|
+
require 'lhm/printer'
|
|
4
|
+
require 'logger'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
describe Lhm::Printer do
|
|
9
|
+
include UnitHelper
|
|
10
|
+
|
|
11
|
+
describe 'percentage printer' do
|
|
12
|
+
|
|
13
|
+
before(:each) do
|
|
14
|
+
@printer = Lhm::Printer::Percentage.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'prints the percentage' do
|
|
18
|
+
r, w = IO.pipe
|
|
19
|
+
Lhm.logger = Logger.new(w)
|
|
20
|
+
|
|
21
|
+
10.times do |i|
|
|
22
|
+
@printer.notify(i, 10)
|
|
23
|
+
assert_match(/#{i}\/10/, log_expression_message(r.gets))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'always prints a bigger message' do
|
|
28
|
+
@length = 0
|
|
29
|
+
|
|
30
|
+
def assert_length(printer)
|
|
31
|
+
new_length = printer.instance_variable_get(:@max_length)
|
|
32
|
+
assert new_length >= @length
|
|
33
|
+
@length = new_length
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@printer.notify(10, 100)
|
|
37
|
+
assert_length(@printer)
|
|
38
|
+
@printer.notify(0, 100)
|
|
39
|
+
assert_length(@printer)
|
|
40
|
+
@printer.notify(1, 1000000)
|
|
41
|
+
assert_length(@printer)
|
|
42
|
+
@printer.notify(0, 0)
|
|
43
|
+
assert_length(@printer)
|
|
44
|
+
@printer.notify(0, nil)
|
|
45
|
+
assert_length(@printer)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'prints the end message' do
|
|
49
|
+
r, w = IO.pipe
|
|
50
|
+
Lhm.logger = Logger.new(w)
|
|
51
|
+
@printer.end
|
|
52
|
+
|
|
53
|
+
assert_equal(log_expression_message(r.gets), "100% complete\n")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'prints the exception message' do
|
|
57
|
+
r, w = IO.pipe
|
|
58
|
+
Lhm.logger = Logger.new(w)
|
|
59
|
+
e = StandardError.new('woops')
|
|
60
|
+
@printer.exception(e)
|
|
61
|
+
|
|
62
|
+
assert_equal(log_expression_message(r.gets), "failed: #{e}\n")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe 'dot printer' do
|
|
67
|
+
|
|
68
|
+
before(:each) do
|
|
69
|
+
@printer = Lhm::Printer::Dot.new
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'prints the dots' do
|
|
73
|
+
mock = MiniTest::Mock.new
|
|
74
|
+
10.times do
|
|
75
|
+
mock.expect(:write, :return_value, ['.'])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@printer.instance_variable_set(:@output, mock)
|
|
79
|
+
10.times { @printer.notify }
|
|
80
|
+
|
|
81
|
+
mock.verify
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/sql_helper'
|
|
7
|
+
|
|
8
|
+
describe Lhm::SqlHelper do
|
|
9
|
+
it 'should name index with a single column' do
|
|
10
|
+
value(Lhm::SqlHelper.idx_name(:users, :name))
|
|
11
|
+
.must_equal('index_users_on_name')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'should name index with multiple columns' do
|
|
15
|
+
value(Lhm::SqlHelper.idx_name(:users, [:name, :firstname]))
|
|
16
|
+
.must_equal('index_users_on_name_and_firstname')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'should name index with prefixed column' do
|
|
20
|
+
value(Lhm::SqlHelper.idx_name(:tracks, ['title(10)', 'album']))
|
|
21
|
+
.must_equal('index_tracks_on_title_and_album')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'should quote column names in index specification' do
|
|
25
|
+
value(Lhm::SqlHelper.idx_spec(['title(10)', 'album']))
|
|
26
|
+
.must_equal('`title`(10), `album`')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
describe Lhm::TableName do
|
|
4
|
+
describe "#archived" do
|
|
5
|
+
it "prefixes and timestamps the old table" do
|
|
6
|
+
subject = Lhm::TableName.new("original", Time.new(2000,01,02,03,04,05))
|
|
7
|
+
assert_equal "lhma_2000_01_02_03_04_05_000_original", subject.archived
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "truncates names below 64 characters" do
|
|
11
|
+
subject = Lhm::TableName.new("some_very_long_original_table_name_that_exceeds_64_characters", Time.new(2000,01,02,03,04,05))
|
|
12
|
+
assert_equal "lhma_2000_01_02_03_04_05_000_some_very_long_original_table_name_", subject.archived
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe "#failed" do
|
|
17
|
+
it "prefixes and postfixes and timestamps the old table" do
|
|
18
|
+
subject = Lhm::TableName.new("original", Time.new(2000,01,02,03,04,05))
|
|
19
|
+
assert_equal "lhma_2000_01_02_03_04_05_000_original_failed", subject.failed
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "truncates names below 64 characters" do
|
|
23
|
+
subject = Lhm::TableName.new("some_very_long_original_table_name_that_exceeds_64_characters", Time.new(2000,01,02,03,04,05))
|
|
24
|
+
assert_equal "lhma_2000_01_02_03_04_05_000_some_very_long_original_tabl_failed", subject.failed
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "#new" do
|
|
29
|
+
it "prefixes and postfixes and timestamps the old table" do
|
|
30
|
+
subject = Lhm::TableName.new("original")
|
|
31
|
+
assert_equal "lhmn_original", subject.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "truncates names below 64 characters" do
|
|
35
|
+
subject = Lhm::TableName.new("some_very_long_original_table_name_that_exceeds_64_characters")
|
|
36
|
+
assert_equal "lhmn_some_very_long_original_table_name_that_exceeds_64_characte", subject.new
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/table'
|
|
7
|
+
|
|
8
|
+
describe Lhm::Table do
|
|
9
|
+
include UnitHelper
|
|
10
|
+
|
|
11
|
+
describe 'names' do
|
|
12
|
+
it 'should name destination' do
|
|
13
|
+
@table = Lhm::Table.new('users')
|
|
14
|
+
value(@table.destination_name).must_equal 'lhmn_users'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe 'constraints' do
|
|
19
|
+
def set_columns(table, columns)
|
|
20
|
+
table.instance_variable_set('@columns', columns)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'should be satisfied with a single column primary key called id' do
|
|
24
|
+
@table = Lhm::Table.new('table', 'id')
|
|
25
|
+
set_columns(@table, { 'id' => { :type => 'int(1)' } })
|
|
26
|
+
value(@table.satisfies_id_column_requirement?).must_equal true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'should be satisfied with a primary key not called id, as long as there is still an id' do
|
|
30
|
+
@table = Lhm::Table.new('table', 'uuid')
|
|
31
|
+
set_columns(@table, { 'id' => { :type => 'int(1)' } })
|
|
32
|
+
value(@table.satisfies_id_column_requirement?).must_equal true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'should be satisifed if display attributes are not present (deprecated in mysql 8)' do
|
|
36
|
+
@table = Lhm::Table.new('table', 'id')
|
|
37
|
+
set_columns(@table, { 'id' => { :type => 'int' } })
|
|
38
|
+
value(@table.satisfies_id_column_requirement?).must_equal true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'should not be satisfied if id is not numeric' do
|
|
42
|
+
@table = Lhm::Table.new('table', 'id')
|
|
43
|
+
set_columns(@table, { 'id' => { :type => 'varchar(255)' } })
|
|
44
|
+
value(@table.satisfies_id_column_requirement?).must_equal false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|