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.
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