lhm-shopify 3.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +34 -0
  3. data/.gitignore +17 -0
  4. data/.rubocop.yml +183 -0
  5. data/.travis.yml +21 -0
  6. data/CHANGELOG.md +216 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +27 -0
  9. data/README.md +284 -0
  10. data/Rakefile +22 -0
  11. data/bin/.gitkeep +0 -0
  12. data/dbdeployer/config.json +32 -0
  13. data/dbdeployer/install.sh +64 -0
  14. data/dev.yml +20 -0
  15. data/gemfiles/ar-2.3_mysql.gemfile +6 -0
  16. data/gemfiles/ar-3.2_mysql.gemfile +5 -0
  17. data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
  18. data/gemfiles/ar-4.0_mysql2.gemfile +5 -0
  19. data/gemfiles/ar-4.1_mysql2.gemfile +5 -0
  20. data/gemfiles/ar-4.2_mysql2.gemfile +5 -0
  21. data/gemfiles/ar-5.0_mysql2.gemfile +5 -0
  22. data/lhm.gemspec +34 -0
  23. data/lib/lhm.rb +131 -0
  24. data/lib/lhm/atomic_switcher.rb +52 -0
  25. data/lib/lhm/chunk_finder.rb +32 -0
  26. data/lib/lhm/chunk_insert.rb +51 -0
  27. data/lib/lhm/chunker.rb +87 -0
  28. data/lib/lhm/cleanup/current.rb +74 -0
  29. data/lib/lhm/command.rb +48 -0
  30. data/lib/lhm/entangler.rb +117 -0
  31. data/lib/lhm/intersection.rb +51 -0
  32. data/lib/lhm/invoker.rb +98 -0
  33. data/lib/lhm/locked_switcher.rb +74 -0
  34. data/lib/lhm/migration.rb +43 -0
  35. data/lib/lhm/migrator.rb +237 -0
  36. data/lib/lhm/printer.rb +59 -0
  37. data/lib/lhm/railtie.rb +9 -0
  38. data/lib/lhm/sql_helper.rb +77 -0
  39. data/lib/lhm/sql_retry.rb +61 -0
  40. data/lib/lhm/table.rb +121 -0
  41. data/lib/lhm/table_name.rb +23 -0
  42. data/lib/lhm/test_support.rb +35 -0
  43. data/lib/lhm/throttler.rb +36 -0
  44. data/lib/lhm/throttler/slave_lag.rb +145 -0
  45. data/lib/lhm/throttler/threads_running.rb +53 -0
  46. data/lib/lhm/throttler/time.rb +29 -0
  47. data/lib/lhm/timestamp.rb +11 -0
  48. data/lib/lhm/version.rb +6 -0
  49. data/shipit.rubygems.yml +0 -0
  50. data/spec/.lhm.example +4 -0
  51. data/spec/README.md +58 -0
  52. data/spec/fixtures/bigint_table.ddl +4 -0
  53. data/spec/fixtures/composite_primary_key.ddl +7 -0
  54. data/spec/fixtures/custom_primary_key.ddl +6 -0
  55. data/spec/fixtures/destination.ddl +6 -0
  56. data/spec/fixtures/lines.ddl +7 -0
  57. data/spec/fixtures/origin.ddl +6 -0
  58. data/spec/fixtures/permissions.ddl +5 -0
  59. data/spec/fixtures/small_table.ddl +4 -0
  60. data/spec/fixtures/tracks.ddl +5 -0
  61. data/spec/fixtures/users.ddl +14 -0
  62. data/spec/fixtures/wo_id_int_column.ddl +6 -0
  63. data/spec/integration/atomic_switcher_spec.rb +93 -0
  64. data/spec/integration/chunk_insert_spec.rb +29 -0
  65. data/spec/integration/chunker_spec.rb +185 -0
  66. data/spec/integration/cleanup_spec.rb +136 -0
  67. data/spec/integration/entangler_spec.rb +66 -0
  68. data/spec/integration/integration_helper.rb +237 -0
  69. data/spec/integration/invoker_spec.rb +33 -0
  70. data/spec/integration/lhm_spec.rb +585 -0
  71. data/spec/integration/lock_wait_timeout_spec.rb +30 -0
  72. data/spec/integration/locked_switcher_spec.rb +50 -0
  73. data/spec/integration/sql_retry/lock_wait_spec.rb +125 -0
  74. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +101 -0
  75. data/spec/integration/table_spec.rb +91 -0
  76. data/spec/test_helper.rb +32 -0
  77. data/spec/unit/atomic_switcher_spec.rb +31 -0
  78. data/spec/unit/chunk_finder_spec.rb +73 -0
  79. data/spec/unit/chunk_insert_spec.rb +44 -0
  80. data/spec/unit/chunker_spec.rb +166 -0
  81. data/spec/unit/entangler_spec.rb +124 -0
  82. data/spec/unit/intersection_spec.rb +51 -0
  83. data/spec/unit/lhm_spec.rb +29 -0
  84. data/spec/unit/locked_switcher_spec.rb +51 -0
  85. data/spec/unit/migrator_spec.rb +146 -0
  86. data/spec/unit/printer_spec.rb +97 -0
  87. data/spec/unit/sql_helper_spec.rb +32 -0
  88. data/spec/unit/table_name_spec.rb +39 -0
  89. data/spec/unit/table_spec.rb +47 -0
  90. data/spec/unit/throttler/slave_lag_spec.rb +317 -0
  91. data/spec/unit/throttler/threads_running_spec.rb +64 -0
  92. data/spec/unit/throttler_spec.rb +124 -0
  93. data/spec/unit/unit_helper.rb +13 -0
  94. 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
@@ -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