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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +43 -0
  3. data/.gitignore +12 -0
  4. data/.rubocop.yml +183 -0
  5. data/.travis.yml +21 -0
  6. data/Appraisals +24 -0
  7. data/CHANGELOG.md +254 -0
  8. data/Gemfile +5 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE +27 -0
  11. data/README.md +335 -0
  12. data/Rakefile +33 -0
  13. data/dev.yml +45 -0
  14. data/docker-compose.yml +60 -0
  15. data/gemfiles/activerecord_5.2.gemfile +9 -0
  16. data/gemfiles/activerecord_5.2.gemfile.lock +66 -0
  17. data/gemfiles/activerecord_6.0.gemfile +7 -0
  18. data/gemfiles/activerecord_6.0.gemfile.lock +68 -0
  19. data/gemfiles/activerecord_6.1.gemfile +7 -0
  20. data/gemfiles/activerecord_6.1.gemfile.lock +67 -0
  21. data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
  22. data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +65 -0
  23. data/lhm.gemspec +38 -0
  24. data/lib/lhm/atomic_switcher.rb +46 -0
  25. data/lib/lhm/chunk_finder.rb +62 -0
  26. data/lib/lhm/chunk_insert.rb +61 -0
  27. data/lib/lhm/chunker.rb +95 -0
  28. data/lib/lhm/cleanup/current.rb +71 -0
  29. data/lib/lhm/command.rb +48 -0
  30. data/lib/lhm/connection.rb +108 -0
  31. data/lib/lhm/entangler.rb +112 -0
  32. data/lib/lhm/intersection.rb +51 -0
  33. data/lib/lhm/invoker.rb +100 -0
  34. data/lib/lhm/locked_switcher.rb +76 -0
  35. data/lib/lhm/migration.rb +51 -0
  36. data/lib/lhm/migrator.rb +244 -0
  37. data/lib/lhm/printer.rb +63 -0
  38. data/lib/lhm/proxysql_helper.rb +10 -0
  39. data/lib/lhm/railtie.rb +9 -0
  40. data/lib/lhm/sql_helper.rb +77 -0
  41. data/lib/lhm/sql_retry.rb +180 -0
  42. data/lib/lhm/table.rb +121 -0
  43. data/lib/lhm/table_name.rb +23 -0
  44. data/lib/lhm/test_support.rb +35 -0
  45. data/lib/lhm/throttler/slave_lag.rb +162 -0
  46. data/lib/lhm/throttler/threads_running.rb +53 -0
  47. data/lib/lhm/throttler/time.rb +29 -0
  48. data/lib/lhm/throttler.rb +36 -0
  49. data/lib/lhm/timestamp.rb +11 -0
  50. data/lib/lhm/version.rb +6 -0
  51. data/lib/lhm-shopify.rb +1 -0
  52. data/lib/lhm.rb +156 -0
  53. data/scripts/helpers/wait-for-dbs.sh +21 -0
  54. data/scripts/mysql/reader/create_replication.sql +10 -0
  55. data/scripts/mysql/writer/create_test_db.sql +1 -0
  56. data/scripts/mysql/writer/create_users.sql +6 -0
  57. data/scripts/proxysql/proxysql.cnf +117 -0
  58. data/shipit.rubygems.yml +0 -0
  59. data/spec/.lhm.example +4 -0
  60. data/spec/README.md +58 -0
  61. data/spec/fixtures/bigint_table.ddl +4 -0
  62. data/spec/fixtures/composite_primary_key.ddl +6 -0
  63. data/spec/fixtures/composite_primary_key_dest.ddl +6 -0
  64. data/spec/fixtures/custom_primary_key.ddl +6 -0
  65. data/spec/fixtures/custom_primary_key_dest.ddl +6 -0
  66. data/spec/fixtures/destination.ddl +6 -0
  67. data/spec/fixtures/lines.ddl +7 -0
  68. data/spec/fixtures/origin.ddl +6 -0
  69. data/spec/fixtures/permissions.ddl +5 -0
  70. data/spec/fixtures/small_table.ddl +4 -0
  71. data/spec/fixtures/tracks.ddl +5 -0
  72. data/spec/fixtures/users.ddl +14 -0
  73. data/spec/fixtures/wo_id_int_column.ddl +6 -0
  74. data/spec/integration/atomic_switcher_spec.rb +129 -0
  75. data/spec/integration/chunk_insert_spec.rb +30 -0
  76. data/spec/integration/chunker_spec.rb +269 -0
  77. data/spec/integration/cleanup_spec.rb +147 -0
  78. data/spec/integration/database.yml +25 -0
  79. data/spec/integration/entangler_spec.rb +68 -0
  80. data/spec/integration/integration_helper.rb +252 -0
  81. data/spec/integration/invoker_spec.rb +33 -0
  82. data/spec/integration/lhm_spec.rb +659 -0
  83. data/spec/integration/lock_wait_timeout_spec.rb +30 -0
  84. data/spec/integration/locked_switcher_spec.rb +50 -0
  85. data/spec/integration/proxysql_spec.rb +34 -0
  86. data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
  87. data/spec/integration/sql_retry/lock_wait_spec.rb +127 -0
  88. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +114 -0
  89. data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
  90. data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
  91. data/spec/integration/table_spec.rb +83 -0
  92. data/spec/integration/toxiproxy_helper.rb +40 -0
  93. data/spec/test_helper.rb +69 -0
  94. data/spec/unit/atomic_switcher_spec.rb +29 -0
  95. data/spec/unit/chunk_finder_spec.rb +73 -0
  96. data/spec/unit/chunk_insert_spec.rb +67 -0
  97. data/spec/unit/chunker_spec.rb +176 -0
  98. data/spec/unit/connection_spec.rb +111 -0
  99. data/spec/unit/entangler_spec.rb +187 -0
  100. data/spec/unit/intersection_spec.rb +51 -0
  101. data/spec/unit/lhm_spec.rb +46 -0
  102. data/spec/unit/locked_switcher_spec.rb +46 -0
  103. data/spec/unit/migrator_spec.rb +144 -0
  104. data/spec/unit/printer_spec.rb +85 -0
  105. data/spec/unit/sql_helper_spec.rb +28 -0
  106. data/spec/unit/table_name_spec.rb +39 -0
  107. data/spec/unit/table_spec.rb +47 -0
  108. data/spec/unit/throttler/slave_lag_spec.rb +322 -0
  109. data/spec/unit/throttler/threads_running_spec.rb +64 -0
  110. data/spec/unit/throttler_spec.rb +124 -0
  111. data/spec/unit/unit_helper.rb +26 -0
  112. 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