lhm-shopify 3.3.5
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 +34 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +183 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +216 -0
- data/Gemfile +5 -0
- data/LICENSE +27 -0
- data/README.md +284 -0
- data/Rakefile +22 -0
- data/bin/.gitkeep +0 -0
- data/dbdeployer/config.json +32 -0
- data/dbdeployer/install.sh +64 -0
- data/dev.yml +20 -0
- data/gemfiles/ar-2.3_mysql.gemfile +6 -0
- data/gemfiles/ar-3.2_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
- data/gemfiles/ar-4.0_mysql2.gemfile +5 -0
- data/gemfiles/ar-4.1_mysql2.gemfile +5 -0
- data/gemfiles/ar-4.2_mysql2.gemfile +5 -0
- data/gemfiles/ar-5.0_mysql2.gemfile +5 -0
- data/lhm.gemspec +34 -0
- data/lib/lhm.rb +131 -0
- data/lib/lhm/atomic_switcher.rb +52 -0
- data/lib/lhm/chunk_finder.rb +32 -0
- data/lib/lhm/chunk_insert.rb +51 -0
- data/lib/lhm/chunker.rb +87 -0
- data/lib/lhm/cleanup/current.rb +74 -0
- data/lib/lhm/command.rb +48 -0
- data/lib/lhm/entangler.rb +117 -0
- data/lib/lhm/intersection.rb +51 -0
- data/lib/lhm/invoker.rb +98 -0
- data/lib/lhm/locked_switcher.rb +74 -0
- data/lib/lhm/migration.rb +43 -0
- data/lib/lhm/migrator.rb +237 -0
- data/lib/lhm/printer.rb +59 -0
- data/lib/lhm/railtie.rb +9 -0
- data/lib/lhm/sql_helper.rb +77 -0
- data/lib/lhm/sql_retry.rb +61 -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.rb +36 -0
- data/lib/lhm/throttler/slave_lag.rb +145 -0
- data/lib/lhm/throttler/threads_running.rb +53 -0
- data/lib/lhm/throttler/time.rb +29 -0
- data/lib/lhm/timestamp.rb +11 -0
- data/lib/lhm/version.rb +6 -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 +7 -0
- data/spec/fixtures/custom_primary_key.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 +93 -0
- data/spec/integration/chunk_insert_spec.rb +29 -0
- data/spec/integration/chunker_spec.rb +185 -0
- data/spec/integration/cleanup_spec.rb +136 -0
- data/spec/integration/entangler_spec.rb +66 -0
- data/spec/integration/integration_helper.rb +237 -0
- data/spec/integration/invoker_spec.rb +33 -0
- data/spec/integration/lhm_spec.rb +585 -0
- data/spec/integration/lock_wait_timeout_spec.rb +30 -0
- data/spec/integration/locked_switcher_spec.rb +50 -0
- data/spec/integration/sql_retry/lock_wait_spec.rb +125 -0
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +101 -0
- data/spec/integration/table_spec.rb +91 -0
- data/spec/test_helper.rb +32 -0
- data/spec/unit/atomic_switcher_spec.rb +31 -0
- data/spec/unit/chunk_finder_spec.rb +73 -0
- data/spec/unit/chunk_insert_spec.rb +44 -0
- data/spec/unit/chunker_spec.rb +166 -0
- data/spec/unit/entangler_spec.rb +124 -0
- data/spec/unit/intersection_spec.rb +51 -0
- data/spec/unit/lhm_spec.rb +29 -0
- data/spec/unit/locked_switcher_spec.rb +51 -0
- data/spec/unit/migrator_spec.rb +146 -0
- data/spec/unit/printer_spec.rb +97 -0
- data/spec/unit/sql_helper_spec.rb +32 -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 +317 -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 +13 -0
- metadata +239 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
|
2
|
+
|
3
|
+
describe Lhm do
|
4
|
+
include IntegrationHelper
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
connect_master!
|
8
|
+
table_create(:users)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'set_session_lock_wait_timeouts should set the sessions lock wait timeouts to less than the global values by a delta' do
|
12
|
+
connection = Lhm.send(:connection)
|
13
|
+
connection.execute('SET GLOBAL innodb_lock_wait_timeout=11')
|
14
|
+
connection.execute('SET GLOBAL lock_wait_timeout=11')
|
15
|
+
connection.execute('SET SESSION innodb_lock_wait_timeout=1')
|
16
|
+
connection.execute('SET SESSION lock_wait_timeout=1')
|
17
|
+
|
18
|
+
global_innodb_lock_wait_timeout = connection.select_one("SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout'")['Value'].to_i
|
19
|
+
global_lock_wait_timeout = connection.select_one("SHOW GLOBAL VARIABLES LIKE 'lock_wait_timeout'")['Value'].to_i
|
20
|
+
|
21
|
+
invoker = Lhm::Invoker.new(Lhm::Table.parse(:users, connection), connection)
|
22
|
+
invoker.set_session_lock_wait_timeouts
|
23
|
+
|
24
|
+
session_innodb_lock_wait_timeout = connection.select_one("SHOW SESSION VARIABLES LIKE 'innodb_lock_wait_timeout'")['Value'].to_i
|
25
|
+
session_lock_wait_timeout = connection.select_one("SHOW SESSION VARIABLES LIKE 'lock_wait_timeout'")['Value'].to_i
|
26
|
+
|
27
|
+
session_lock_wait_timeout.must_equal global_lock_wait_timeout + Lhm::Invoker::LOCK_WAIT_TIMEOUT_DELTA
|
28
|
+
session_innodb_lock_wait_timeout.must_equal global_innodb_lock_wait_timeout + Lhm::Invoker::LOCK_WAIT_TIMEOUT_DELTA
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
|
5
|
+
|
6
|
+
require 'lhm/table'
|
7
|
+
require 'lhm/migration'
|
8
|
+
require 'lhm/locked_switcher'
|
9
|
+
|
10
|
+
describe Lhm::LockedSwitcher do
|
11
|
+
include IntegrationHelper
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
connect_master!
|
15
|
+
@old_logger = Lhm.logger
|
16
|
+
Lhm.logger = Logger.new('/dev/null')
|
17
|
+
end
|
18
|
+
|
19
|
+
after(:each) do
|
20
|
+
Lhm.logger = @old_logger
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'switching' do
|
24
|
+
before(:each) do
|
25
|
+
@origin = table_create('origin')
|
26
|
+
@destination = table_create('destination')
|
27
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'rename origin to archive' do
|
31
|
+
switcher = Lhm::LockedSwitcher.new(@migration, connection)
|
32
|
+
switcher.run
|
33
|
+
|
34
|
+
slave do
|
35
|
+
data_source_exists?(@origin).must_equal true
|
36
|
+
table_read(@migration.archive_name).columns.keys.must_include 'origin'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'rename destination to origin' do
|
41
|
+
switcher = Lhm::LockedSwitcher.new(@migration, connection)
|
42
|
+
switcher.run
|
43
|
+
|
44
|
+
slave do
|
45
|
+
data_source_exists?(@destination).must_equal false
|
46
|
+
table_read(@origin.name).columns.keys.must_include 'destination'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'mysql2'
|
3
|
+
require 'integration/sql_retry/lock_wait_timeout_test_helper'
|
4
|
+
require 'lhm'
|
5
|
+
|
6
|
+
describe Lhm::SqlRetry do
|
7
|
+
before(:each) do
|
8
|
+
@old_logger = Lhm.logger
|
9
|
+
@logger = StringIO.new
|
10
|
+
Lhm.logger = Logger.new(@logger)
|
11
|
+
|
12
|
+
@helper = LockWaitTimeoutTestHelper.new(
|
13
|
+
lock_duration: 5,
|
14
|
+
innodb_lock_wait_timeout: 2
|
15
|
+
)
|
16
|
+
|
17
|
+
@helper.create_table_to_lock
|
18
|
+
|
19
|
+
# Start a thread to hold a lock on the table
|
20
|
+
@locked_record_id = @helper.hold_lock
|
21
|
+
|
22
|
+
# Assert our pre-conditions
|
23
|
+
assert_equal 2, @helper.record_count
|
24
|
+
end
|
25
|
+
|
26
|
+
after(:each) do
|
27
|
+
# Restore default logger
|
28
|
+
Lhm.logger = @old_logger
|
29
|
+
end
|
30
|
+
|
31
|
+
# This is the control test case. It shows that when Lhm::SqlRetry is not used,
|
32
|
+
# a lock wait timeout exceeded error is raised.
|
33
|
+
it "does nothing to prevent exceptions, when not used" do
|
34
|
+
puts ""
|
35
|
+
puts "***The output you see below is OK so long as the test passes.***"
|
36
|
+
puts "*" * 64
|
37
|
+
# Start a thread to retry, once the lock is held, execute the block
|
38
|
+
@helper.with_waiting_lock do |waiting_connection|
|
39
|
+
@helper.insert_records_at_ids(waiting_connection, [@locked_record_id])
|
40
|
+
end
|
41
|
+
|
42
|
+
exception = assert_raises { @helper.trigger_wait_lock }
|
43
|
+
|
44
|
+
assert_equal "Lock wait timeout exceeded; try restarting transaction", exception.message
|
45
|
+
assert_equal Mysql2::Error::TimeoutError, exception.class
|
46
|
+
|
47
|
+
assert_equal 2, @helper.record_count # no records inserted
|
48
|
+
puts "*" * 64
|
49
|
+
end
|
50
|
+
|
51
|
+
# This is test demonstrating the happy path: a well configured retry
|
52
|
+
# tuned to the locks it encounters.
|
53
|
+
it "successfully executes the SQL despite the errors encountered" do
|
54
|
+
# Start a thread to retry, once the lock is held, execute the block
|
55
|
+
@helper.with_waiting_lock do |waiting_connection|
|
56
|
+
sql_retry = Lhm::SqlRetry.new(waiting_connection, {
|
57
|
+
base_interval: 0.2, # first retry after 200ms
|
58
|
+
multiplier: 1, # subsequent retries wait 1x longer than first retry (no change)
|
59
|
+
tries: 3, # we only need 3 tries (including the first) for the scenario described below
|
60
|
+
rand_factor: 0 # do not introduce randomness to wait timer
|
61
|
+
})
|
62
|
+
|
63
|
+
# RetryTestHelper is configured to hold lock for 5 seconds and timeout after 2 seconds.
|
64
|
+
# Therefore the sequence of events will be:
|
65
|
+
# 0s: first insert query is started while lock is held
|
66
|
+
# 2s: first timeout error will occur, SqlRetry is configured to wait 200ms after this
|
67
|
+
# 2.2s: second insert query is started while lock is held
|
68
|
+
# 4.2s: second timeout error will occur, SqlRetry is configured to wait 200ms after this
|
69
|
+
# 4.4s: third insert query is started while lock is held
|
70
|
+
# 5s: lock is released, insert successful no further retries needed
|
71
|
+
sql_retry.with_retries do |retriable_connection|
|
72
|
+
@helper.insert_records_at_ids(retriable_connection, [@locked_record_id])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
@helper.trigger_wait_lock
|
77
|
+
|
78
|
+
assert_equal 3, @helper.record_count # records inserted successfully despite lock
|
79
|
+
|
80
|
+
logs = @logger.string.split("\n")
|
81
|
+
assert_equal 2, logs.length
|
82
|
+
|
83
|
+
assert logs.first.include?("Mysql2::Error::TimeoutError: 'Lock wait timeout exceeded; try restarting transaction' - 1 tries")
|
84
|
+
assert logs.first.include?("0.2 seconds until the next try")
|
85
|
+
|
86
|
+
assert logs.last.include?("Mysql2::Error::TimeoutError: 'Lock wait timeout exceeded; try restarting transaction' - 2 tries")
|
87
|
+
assert logs.last.include?("0.2 seconds until the next try")
|
88
|
+
end
|
89
|
+
|
90
|
+
# This is test demonstrating the sad configuration path: it shows
|
91
|
+
# that when the retries are not tuned to the locks encountered,
|
92
|
+
# retries are not effective.
|
93
|
+
it "fails to retry enough to overcome the timeout" do
|
94
|
+
puts ""
|
95
|
+
puts "***The output you see below is OK so long as the test passes.***"
|
96
|
+
puts "*" * 64
|
97
|
+
# Start a thread to retry, once the lock is held, execute the block
|
98
|
+
@helper.with_waiting_lock do |waiting_connection|
|
99
|
+
sql_retry = Lhm::SqlRetry.new(waiting_connection, {
|
100
|
+
base_interval: 0.2, # first retry after 200ms
|
101
|
+
multiplier: 1, # subsequent retries wait 1x longer than first retry (no change)
|
102
|
+
tries: 2, # we need 3 tries (including the first) for the scenario described below, but we only get two...we will fail
|
103
|
+
rand_factor: 0 # do not introduce randomness to wait timer
|
104
|
+
})
|
105
|
+
|
106
|
+
# RetryTestHelper is configured to hold lock for 5 seconds and timeout after 2 seconds.
|
107
|
+
# Therefore the sequence of events will be:
|
108
|
+
# 0s: first insert query is started while lock is held
|
109
|
+
# 2s: first timeout error will occur, SqlRetry is configured to wait 200ms after this
|
110
|
+
# 2.2s: second insert query is started while lock is held
|
111
|
+
# 4.2s: second timeout error will occur, SqlRetry is configured to only try twice, so we fail here
|
112
|
+
sql_retry.with_retries do |retriable_connection|
|
113
|
+
@helper.insert_records_at_ids(retriable_connection, [@locked_record_id])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
exception = assert_raises { @helper.trigger_wait_lock }
|
118
|
+
|
119
|
+
assert_equal "Lock wait timeout exceeded; try restarting transaction", exception.message
|
120
|
+
assert_equal Mysql2::Error::TimeoutError, exception.class
|
121
|
+
|
122
|
+
assert_equal 2, @helper.record_count # no records inserted
|
123
|
+
puts "*" * 64
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
class LockWaitTimeoutTestHelper
|
3
|
+
def initialize(lock_duration:, innodb_lock_wait_timeout:)
|
4
|
+
# This connection will be used exclusively to setup the test,
|
5
|
+
# assert pre-conditions and assert post-conditions.
|
6
|
+
# We choose to use a `Mysql2::Client` connection instead of
|
7
|
+
# `ActiveRecord::Base.establish_connection` because of AR's connection
|
8
|
+
# pool which forces thread syncronization. In this test,
|
9
|
+
# we want to intentionally create a lock to test retries,
|
10
|
+
# so that is an anti-feature.
|
11
|
+
@main_conn = new_mysql_connection
|
12
|
+
|
13
|
+
@lock_duration = lock_duration
|
14
|
+
|
15
|
+
# While implementing this, I discovered that MySQL seems to have an off-by-one
|
16
|
+
# bug with the innodb_lock_wait_timeout. If you ask it to wait 2 seconds, it will wait 3.
|
17
|
+
# In order to avoid surprisingly the user, let's account for that here, but also
|
18
|
+
# guard against a case where we go below 1, the minimum value.
|
19
|
+
raise ArgumentError, "innodb_lock_wait_timeout must be greater than or equal to 2" unless innodb_lock_wait_timeout >= 2
|
20
|
+
raise ArgumentError, "innodb_lock_wait_timeout must be an integer" if innodb_lock_wait_timeout.class != Integer
|
21
|
+
@innodb_lock_wait_timeout = innodb_lock_wait_timeout - 1
|
22
|
+
|
23
|
+
@threads = []
|
24
|
+
@queue = Queue.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_table_to_lock(connection = main_conn)
|
28
|
+
connection.query("DROP TABLE IF EXISTS #{test_table_name};")
|
29
|
+
connection.query("CREATE TABLE #{test_table_name} (id INT, PRIMARY KEY (id)) ENGINE=InnoDB;")
|
30
|
+
end
|
31
|
+
|
32
|
+
def hold_lock(seconds = lock_duration, queue = @queue)
|
33
|
+
# We are intentionally choosing to create a gap in the between the IDs to
|
34
|
+
# create a gap lock.
|
35
|
+
insert_records_at_ids(main_conn, [1001,1003])
|
36
|
+
locked_id = 1002
|
37
|
+
|
38
|
+
# This is the locking thread. It creates gap lock. It must be created first.
|
39
|
+
@threads << Thread.new do
|
40
|
+
conn = new_mysql_connection
|
41
|
+
conn.query("START TRANSACTION;")
|
42
|
+
conn.query("DELETE FROM #{test_table_name} WHERE id=#{locked_id}") # we now have the lock
|
43
|
+
queue.push(true) # this will signal the waiting thread to unblock, now that the lock is held
|
44
|
+
sleep seconds # hold the lock, while the waiting thread is waiting/retrying
|
45
|
+
conn.query("ROLLBACK;") # release the lock
|
46
|
+
end
|
47
|
+
|
48
|
+
return locked_id
|
49
|
+
end
|
50
|
+
|
51
|
+
def record_count(connection = main_conn)
|
52
|
+
response = connection.query("SELECT COUNT(id) FROM #{test_table_name}")
|
53
|
+
response.first.values.first
|
54
|
+
end
|
55
|
+
|
56
|
+
def with_waiting_lock(lock_time = @lock_duration, queue = @queue)
|
57
|
+
@threads << Thread.new do
|
58
|
+
conn = new_mysql_connection
|
59
|
+
conn.query("SET SESSION innodb_lock_wait_timeout = #{innodb_lock_wait_timeout}") # set timeout to be less than lock_time, so the timeout will happen
|
60
|
+
queue.pop # this will block until the lock thread establishes lock
|
61
|
+
yield(conn) # invoke the code that should retry while lock is held
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def trigger_wait_lock
|
66
|
+
@threads.each(&:join)
|
67
|
+
end
|
68
|
+
|
69
|
+
def insert_records_at_ids(connection, ids)
|
70
|
+
ids.each do |id|
|
71
|
+
connection.query "INSERT INTO #{test_table_name} (id) VALUES (#{id})"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
attr_reader :main_conn, :lock_duration, :innodb_lock_wait_timeout
|
78
|
+
|
79
|
+
def new_mysql_connection
|
80
|
+
Mysql2::Client.new(
|
81
|
+
host: '127.0.0.1',
|
82
|
+
database: test_db_name,
|
83
|
+
username: db_config['master']['user'],
|
84
|
+
password: db_config['master']['password'],
|
85
|
+
port: db_config['master']['port'],
|
86
|
+
socket: db_config['master']['socket']
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_db_name
|
91
|
+
@test_db_name ||= "test"
|
92
|
+
end
|
93
|
+
|
94
|
+
def db_config
|
95
|
+
@db_config ||= YAML.load_file(File.expand_path(File.dirname(__FILE__)) + '/../database.yml')
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_table_name
|
99
|
+
@test_table_name ||= "lock_wait"
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
|
5
|
+
require 'lhm/table'
|
6
|
+
|
7
|
+
describe Lhm::Table do
|
8
|
+
include IntegrationHelper
|
9
|
+
|
10
|
+
describe 'id numeric column requirement' do
|
11
|
+
describe 'when met' do
|
12
|
+
before(:each) do
|
13
|
+
connect_master!
|
14
|
+
@table = table_create(:custom_primary_key)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should parse primary key' do
|
18
|
+
@table.pk.must_equal('pk')
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should parse indices' do
|
22
|
+
@table.
|
23
|
+
indices['index_custom_primary_key_on_id'].
|
24
|
+
must_equal(['id'])
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should parse columns' do
|
28
|
+
@table.
|
29
|
+
columns['id'][:type].
|
30
|
+
must_match(/(bigint|int)\(\d+\)/)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should return true for method that should be renamed' do
|
34
|
+
@table.satisfies_id_column_requirement?.must_equal true
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should support bigint tables' do
|
38
|
+
@table = table_create(:bigint_table)
|
39
|
+
@table.satisfies_id_column_requirement?.must_equal true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'when not met' do
|
44
|
+
before(:each) do
|
45
|
+
connect_master!
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should return false for a non-int id column' do
|
49
|
+
@table = table_create(:wo_id_int_column)
|
50
|
+
@table.satisfies_id_column_requirement?.must_equal false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe Lhm::Table::Parser do
|
56
|
+
describe 'create table parsing' do
|
57
|
+
before(:each) do
|
58
|
+
connect_master!
|
59
|
+
@table = table_create(:users)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should parse table name in show create table' do
|
63
|
+
@table.name.must_equal('users')
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should parse primary key' do
|
67
|
+
@table.pk.must_equal('id')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should parse column type in show create table' do
|
71
|
+
@table.columns['username'][:type].must_equal('varchar(255)')
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should parse column metadata' do
|
75
|
+
assert_nil @table.columns['username'][:column_default]
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should parse indices' do
|
79
|
+
@table.
|
80
|
+
indices['index_users_on_username_and_created_at'].
|
81
|
+
must_equal(['username', 'created_at'])
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should parse index' do
|
85
|
+
@table.
|
86
|
+
indices['index_users_on_reference'].
|
87
|
+
must_equal(['reference'])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/spec/test_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
if ENV['COV']
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'minitest/autorun'
|
10
|
+
require 'minitest/spec'
|
11
|
+
require 'minitest/mock'
|
12
|
+
require 'mocha/minitest'
|
13
|
+
require 'pathname'
|
14
|
+
require 'lhm'
|
15
|
+
|
16
|
+
$project = Pathname.new(File.dirname(__FILE__) + '/..').cleanpath
|
17
|
+
$spec = $project.join('spec')
|
18
|
+
$fixtures = $spec.join('fixtures')
|
19
|
+
|
20
|
+
require 'active_record'
|
21
|
+
require 'mysql2'
|
22
|
+
|
23
|
+
logger = Logger.new STDOUT
|
24
|
+
logger.level = Logger::WARN
|
25
|
+
Lhm.logger = logger
|
26
|
+
|
27
|
+
def without_verbose(&block)
|
28
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
29
|
+
yield
|
30
|
+
ensure
|
31
|
+
$VERBOSE = old_verbose
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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/atomic_switcher'
|
9
|
+
|
10
|
+
describe Lhm::AtomicSwitcher 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::AtomicSwitcher.new(@migration, nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'atomic switch' do
|
22
|
+
it 'should perform a single atomic rename' do
|
23
|
+
@switcher.
|
24
|
+
atomic_switch.
|
25
|
+
must_equal(
|
26
|
+
"rename table `origin` to `#{ @migration.archive_name }`, " \
|
27
|
+
'`destination` to `origin`'
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|