lhm 1.1.0 → 1.2.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 (44) hide show
  1. data/.travis.yml +1 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE +1 -1
  4. data/README.md +52 -16
  5. data/Rakefile +0 -1
  6. data/bin/lhm-spec-clobber.sh +3 -3
  7. data/gemfiles/dm_mysql.gemfile +5 -0
  8. data/lhm.gemspec +0 -2
  9. data/lib/lhm.rb +16 -3
  10. data/lib/lhm/atomic_switcher.rb +4 -4
  11. data/lib/lhm/chunker.rb +2 -2
  12. data/lib/lhm/command.rb +1 -1
  13. data/lib/lhm/connection.rb +143 -0
  14. data/lib/lhm/entangler.rb +5 -5
  15. data/lib/lhm/intersection.rb +1 -1
  16. data/lib/lhm/invoker.rb +1 -1
  17. data/lib/lhm/locked_switcher.rb +5 -4
  18. data/lib/lhm/migration.rb +1 -1
  19. data/lib/lhm/migrator.rb +5 -8
  20. data/lib/lhm/sql_helper.rb +14 -22
  21. data/lib/lhm/table.rb +29 -15
  22. data/lib/lhm/version.rb +2 -2
  23. data/spec/bootstrap.rb +1 -1
  24. data/spec/integration/atomic_switcher_spec.rb +3 -3
  25. data/spec/integration/chunker_spec.rb +1 -1
  26. data/spec/integration/entangler_spec.rb +1 -1
  27. data/spec/integration/integration_helper.rb +35 -18
  28. data/spec/integration/lhm_spec.rb +4 -1
  29. data/spec/integration/locked_switcher_spec.rb +1 -1
  30. data/spec/integration/table_spec.rb +1 -1
  31. data/spec/unit/active_record_connection_spec.rb +40 -0
  32. data/spec/unit/atomic_switcher_spec.rb +1 -1
  33. data/spec/unit/chunker_spec.rb +1 -1
  34. data/spec/unit/connection_spec.rb +11 -0
  35. data/spec/unit/datamapper_connection_spec.rb +49 -0
  36. data/spec/unit/entangler_spec.rb +1 -1
  37. data/spec/unit/intersection_spec.rb +1 -1
  38. data/spec/unit/locked_switcher_spec.rb +1 -1
  39. data/spec/unit/migration_spec.rb +1 -1
  40. data/spec/unit/migrator_spec.rb +1 -1
  41. data/spec/unit/sql_helper_spec.rb +1 -1
  42. data/spec/unit/table_spec.rb +1 -1
  43. data/spec/unit/unit_helper.rb +12 -1
  44. metadata +9 -43
@@ -8,3 +8,4 @@ gemfile:
8
8
  - gemfiles/ar-2.3_mysql.gemfile
9
9
  - gemfiles/ar-3.2_mysql.gemfile
10
10
  - gemfiles/ar-3.2_mysql2.gemfile
11
+ - gemfiles/dm_mysql.gemfile
@@ -1,3 +1,9 @@
1
+ # 1.2.0 (February 22, 2013)
2
+
3
+ * Added DataMapper support, no API changes for current users. Refer to the
4
+ README for information.
5
+ * Documentation updates. Thanks @tiegz and @vinbarnes.
6
+
1
7
  # 1.1.0 (April 29, 2012)
2
8
 
3
9
  * Add option to specify custom index name
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011, SoundCloud, Rany Keddo, Tobias Bielohlawek, Tobias Schmidt
1
+ Copyright (c) 2011 - 2013, SoundCloud, Rany Keddo, Tobias Bielohlawek, Tobias Schmidt
2
2
 
3
3
  All rights reserved.
4
4
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png)][4]
1
+ # Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png?branch=master)][4]
2
2
 
3
3
  Rails style database migrations are a useful way to evolve your data schema in
4
4
  an agile manner. Most Rails projects start like this, and at first, making
@@ -22,7 +22,7 @@ is great if you are using this engine, but only solves half the problem.
22
22
  At SoundCloud we started having migration pains quite a while ago, and after
23
23
  looking around for third party solutions, we decided to create our
24
24
  own. We called it Large Hadron Migrator, and it is a gem for online
25
- ActiveRecord migrations.
25
+ ActiveRecord and DataMapper migrations.
26
26
 
27
27
  ![LHC](http://farm4.static.flickr.com/3093/2844971993_17f2ddf2a8_z.jpg)
28
28
 
@@ -35,18 +35,22 @@ without locking the table. In contrast to [OAK][0] and the
35
35
  [facebook tool][1], we only use a copy table and triggers.
36
36
 
37
37
  The Large Hadron is a test driven Ruby solution which can easily be dropped
38
- into an ActiveRecord migration. It presumes a single auto incremented
39
- numerical primary key called id as per the Rails convention. Unlike the
40
- [twitter solution][2], it does not require the presence of an indexed
38
+ into an ActiveRecord or DataMapper migration. It presumes a single auto
39
+ incremented numerical primary key called id as per the Rails convention. Unlike
40
+ the [twitter solution][2], it does not require the presence of an indexed
41
41
  `updated_at` column.
42
42
 
43
43
  ## Requirements
44
44
 
45
45
  Lhm currently only works with MySQL databases and requires an established
46
- ActiveRecord connection.
46
+ ActiveRecord or DataMapper connection.
47
47
 
48
48
  It is compatible and [continuously tested][4] with Ruby 1.8.7 and Ruby 1.9.x,
49
- ActiveRecord 2.3.x and 3.x as well as mysql and mysql2 adapters.
49
+ ActiveRecord 2.3.x and 3.x (mysql and mysql2 adapters), as well as DataMapper
50
+ 1.2 (dm-mysql-adapter).
51
+
52
+ Lhm also works with dm-master-slave-adapter, it'll bind to the master before
53
+ running the migrations.
50
54
 
51
55
  ## Installation
52
56
 
@@ -66,6 +70,10 @@ ActiveRecord::Base.establish_connection(
66
70
  :database => 'lhm'
67
71
  )
68
72
 
73
+ # or with DataMapper
74
+ Lhm.setup(DataMapper.setup(:default, 'mysql://127.0.0.1/lhm'))
75
+
76
+ # and migrate
69
77
  Lhm.change_table :users do |m|
70
78
  m.add_column :arbitrary, "INT(12)"
71
79
  m.add_index [:arbitrary_id, :created_at]
@@ -91,26 +99,54 @@ class MigrateUsers < ActiveRecord::Migration
91
99
  def self.down
92
100
  Lhm.change_table :users do |m|
93
101
  m.remove_index [:arbitrary_id, :created_at]
94
- m.remove_column :arbitrary)
102
+ m.remove_column :arbitrary
103
+ end
104
+ end
105
+ end
106
+ ```
107
+
108
+ Using dm-migrations, you'd define all your migrations as follows, and then call
109
+ `migrate_up!` or `migrate_down!` as normal.
110
+
111
+ ```ruby
112
+ require 'dm-migrations/migration_runner'
113
+ require 'lhm'
114
+
115
+ migration 1, :migrate_users do
116
+ up do
117
+ Lhm.change_table :users do |m|
118
+ m.add_column :arbitrary, "INT(12)"
119
+ m.add_index [:arbitrary_id, :created_at]
120
+ m.ddl("alter table %s add column flag tinyint(1)" % m.name)
121
+ end
122
+ end
123
+
124
+ down do
125
+ Lhm.change_table :users do |m|
126
+ m.remove_index [:arbitrary_id, :created_at]
127
+ m.remove_column :arbitrary
95
128
  end
96
129
  end
97
130
  end
98
131
  ```
99
132
 
133
+ **Note:** Lhm won't delete the old, leftover table. This is on purpose, in order
134
+ to prevent accidental data loss.
135
+
100
136
  ## Table rename strategies
101
137
 
102
138
  There are two different table rename strategies available: LockedSwitcher and
103
139
  AtomicSwitcher.
104
140
 
105
- For all setups which use replication and a MySQL version
106
- affected by the the [binlog bug #39675](http://bugs.mysql.com/bug.php?id=39675),
107
- we recommend the LockedSwitcher strategy to avoid replication issues. This
108
- strategy locks the table being migrated and issues two ALTER TABLE statements.
109
- The AtomicSwitcher uses a single atomic RENAME TABLE query and should be favored
110
- in setups which do not suffer from the mentioned replication bug.
141
+ The LockedSwitcher strategy locks the table being migrated and issues two ALTER TABLE statements.
142
+ The AtomicSwitcher uses a single atomic RENAME TABLE query and is the favored solution.
143
+
144
+ Lhm chooses AtomicSwitcher if no strategy is specified, **unless** your version of MySQL is
145
+ affected by [binlog bug #39675](http://bugs.mysql.com/bug.php?id=39675). If your version is
146
+ affected, Lhm will raise an error if you don't specify a strategy. You're recommended
147
+ to use the LockedSwitcher in these cases to avoid replication issues.
111
148
 
112
- Lhm chooses the strategy automatically based on the used MySQL server version,
113
- but you can override the behavior with an option:
149
+ To specify the strategy in your migration:
114
150
 
115
151
  ```ruby
116
152
  Lhm.change_table :users, :atomic_switch => true do |m|
data/Rakefile CHANGED
@@ -17,4 +17,3 @@ end
17
17
 
18
18
  task :specs => [:unit, :integration]
19
19
  task :default => :specs
20
-
@@ -6,15 +6,15 @@ set -u
6
6
  source ~/.lhm
7
7
 
8
8
  lhmkill() {
9
- ps -ef | gsed -n "/[m]ysqld.*lhm-cluster/p" | awk '{ print $2 }' | xargs kill
10
- sleep 5
9
+ echo killing lhm-cluster
10
+ ps -ef | sed -n "/[m]ysqld.*lhm-cluster/p" | awk '{ print $2 }' | xargs kill
11
+ sleep 2
11
12
  }
12
13
 
13
14
  echo stopping other running mysql instance
14
15
  launchctl remove com.mysql.mysqld || { echo launchctl did not remove mysqld; }
15
16
  "$mysqldir"/bin/mysqladmin shutdown || { echo mysqladmin did not shut down anything; }
16
17
 
17
- echo killing lhm-cluster
18
18
  lhmkill
19
19
 
20
20
  echo removing $basedir
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gem 'dm-core'
4
+ gem 'dm-mysql-adapter'
5
+ gemspec :path=>"../"
@@ -21,7 +21,5 @@ Gem::Specification.new do |s|
21
21
 
22
22
  s.add_development_dependency "minitest", "= 2.10.0"
23
23
  s.add_development_dependency "rake"
24
-
25
- s.add_dependency "activerecord"
26
24
  end
27
25
 
data/lib/lhm.rb CHANGED
@@ -1,9 +1,9 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
- require 'active_record'
5
4
  require 'lhm/table'
6
5
  require 'lhm/invoker'
6
+ require 'lhm/connection'
7
7
  require 'lhm/version'
8
8
 
9
9
  # Large hadron migrator - online schema change tool
@@ -34,7 +34,8 @@ module Lhm
34
34
  # @return [Boolean] Returns true if the migration finishes
35
35
  # @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
36
36
  def self.change_table(table_name, options = {}, &block)
37
- connection = ActiveRecord::Base.connection
37
+ connection = Connection.new(adapter)
38
+
38
39
  origin = Table.parse(table_name, connection)
39
40
  invoker = Invoker.new(origin, connection)
40
41
  block.call(invoker.migrator)
@@ -42,4 +43,16 @@ module Lhm
42
43
 
43
44
  true
44
45
  end
46
+
47
+ def self.setup(adapter)
48
+ @@adapter = adapter
49
+ end
50
+
51
+ def self.adapter
52
+ @@adapter ||=
53
+ begin
54
+ raise 'Please call Lhm.setup' unless defined?(ActiveRecord)
55
+ ActiveRecord::Base.connection
56
+ end
57
+ end
45
58
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'lhm/command'
@@ -13,7 +13,6 @@ module Lhm
13
13
  # Lhm::SqlHelper.supports_atomic_switch?.
14
14
  class AtomicSwitcher
15
15
  include Command
16
- include SqlHelper
17
16
 
18
17
  attr_reader :connection
19
18
 
@@ -36,14 +35,15 @@ module Lhm
36
35
  end
37
36
 
38
37
  def validate
39
- unless table?(@origin.name) && table?(@destination.name)
38
+ unless @connection.table_exists?(@origin.name) &&
39
+ @connection.table_exists?(@destination.name)
40
40
  error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
41
41
  end
42
42
  end
43
43
 
44
44
  private
45
45
  def execute
46
- sql statements
46
+ @connection.sql(statements)
47
47
  end
48
48
  end
49
49
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'lhm/command'
@@ -83,7 +83,7 @@ module Lhm
83
83
 
84
84
  def execute
85
85
  up_to do |lowest, highest|
86
- affected_rows = update(copy(lowest, highest))
86
+ affected_rows = @connection.update(copy(lowest, highest))
87
87
 
88
88
  if affected_rows > 0
89
89
  sleep(throttle_seconds)
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
@@ -0,0 +1,143 @@
1
+ module Lhm
2
+ require 'lhm/sql_helper'
3
+
4
+ class Connection
5
+ def self.new(adapter)
6
+ if defined?(DataMapper) && adapter.is_a?(DataMapper::Adapters::AbstractAdapter)
7
+ DataMapperConnection.new(adapter)
8
+ elsif defined?(ActiveRecord)
9
+ ActiveRecordConnection.new(adapter)
10
+ else
11
+ raise 'Neither DataMapper nor ActiveRecord found.'
12
+ end
13
+ end
14
+
15
+ class DataMapperConnection
16
+ include SqlHelper
17
+
18
+ def initialize(adapter)
19
+ @adapter = adapter
20
+ @database_name = adapter.options['database'] || adapter.options['path'][1..-1]
21
+ end
22
+
23
+ def sql(statements)
24
+ [statements].flatten.each do |statement|
25
+ execute(tagged(statement))
26
+ end
27
+ end
28
+
29
+ def show_create(table_name)
30
+ sql = "show create table `#{ table_name }`"
31
+ select_values(sql).last
32
+ end
33
+
34
+ def current_database
35
+ @database_name
36
+ end
37
+
38
+ def update(statements)
39
+ [statements].flatten.inject(0) do |memo, statement|
40
+ result = @adapter.execute(tagged(statement))
41
+ memo += result.affected_rows
42
+ end
43
+ end
44
+
45
+ def select_all(sql)
46
+ @adapter.select(sql).to_a
47
+ end
48
+
49
+ def select_one(sql)
50
+ select_all(sql).first
51
+ end
52
+
53
+ def select_values(sql)
54
+ select_one(sql).values
55
+ end
56
+
57
+ def select_value(sql)
58
+ select_one(sql)
59
+ end
60
+
61
+ def destination_create(origin)
62
+ original = %{CREATE TABLE "#{ origin.name }"}
63
+ replacement = %{CREATE TABLE "#{ origin.destination_name }"}
64
+
65
+ sql(origin.ddl.gsub(original, replacement))
66
+ end
67
+
68
+ def execute(sql)
69
+ @adapter.execute(sql)
70
+ end
71
+
72
+ def table_exists?(table_name)
73
+ !!select_one(%Q{
74
+ select *
75
+ from information_schema.tables
76
+ where table_schema = '#{ @database_name }'
77
+ and table_name = '#{ table_name }'
78
+ })
79
+ end
80
+ end
81
+
82
+ class ActiveRecordConnection
83
+ include SqlHelper
84
+
85
+ def initialize(adapter)
86
+ @adapter = adapter
87
+ @database_name = @adapter.current_database
88
+ end
89
+
90
+ def sql(statements)
91
+ [statements].flatten.each do |statement|
92
+ execute(tagged(statement))
93
+ end
94
+ end
95
+
96
+ def show_create(table_name)
97
+ sql = "show create table `#{ table_name }`"
98
+ specification = nil
99
+ execute(sql).each { |row| specification = row.last }
100
+ specification
101
+ end
102
+
103
+ def current_database
104
+ @database_name
105
+ end
106
+
107
+ def update(sql)
108
+ @adapter.update(sql)
109
+ end
110
+
111
+ def select_all(sql)
112
+ @adapter.select_all(sql)
113
+ end
114
+
115
+ def select_one(sql)
116
+ @adapter.select_one(sql)
117
+ end
118
+
119
+ def select_values(sql)
120
+ @adapter.select_values(sql)
121
+ end
122
+
123
+ def select_value(sql)
124
+ @adapter.select_value(sql)
125
+ end
126
+
127
+ def destination_create(origin)
128
+ original = %{CREATE TABLE `#{ origin.name }`}
129
+ replacement = %{CREATE TABLE `#{ origin.destination_name }`}
130
+
131
+ sql(origin.ddl.gsub(original, replacement))
132
+ end
133
+
134
+ def execute(sql)
135
+ @adapter.execute(sql)
136
+ end
137
+
138
+ def table_exists?(table_name)
139
+ @adapter.table_exists?(table_name)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'lhm/command'
@@ -68,21 +68,21 @@ module Lhm
68
68
  end
69
69
 
70
70
  def validate
71
- unless table?(@origin.name)
71
+ unless @connection.table_exists?(@origin.name)
72
72
  error("#{ @origin.name } does not exist")
73
73
  end
74
74
 
75
- unless table?(@destination.name)
75
+ unless @connection.table_exists?(@destination.name)
76
76
  error("#{ @destination.name } does not exist")
77
77
  end
78
78
  end
79
79
 
80
80
  def before
81
- sql(entangle)
81
+ @connection.sql(entangle)
82
82
  end
83
83
 
84
84
  def after
85
- sql(untangle)
85
+ @connection.sql(untangle)
86
86
  end
87
87
 
88
88
  def revert
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'lhm/chunker'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'lhm/command'
@@ -53,7 +53,8 @@ module Lhm
53
53
  end
54
54
 
55
55
  def validate
56
- unless table?(@origin.name) && table?(@destination.name)
56
+ unless @connection.table_exists?(@origin.name) &&
57
+ @connection.table_exists?(@destination.name)
57
58
  error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
58
59
  end
59
60
  end
@@ -61,11 +62,11 @@ module Lhm
61
62
  private
62
63
 
63
64
  def revert
64
- sql "unlock tables"
65
+ @connection.sql("unlock tables")
65
66
  end
66
67
 
67
68
  def execute
68
- sql statements
69
+ @connection.sql(statements)
69
70
  end
70
71
  end
71
72
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'lhm/intersection'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'lhm/command'
@@ -142,7 +142,7 @@ module Lhm
142
142
  private
143
143
 
144
144
  def validate
145
- unless table?(@origin.name)
145
+ unless @connection.table_exists?(@origin.name)
146
146
  error("could not find origin table #{ @origin.name }")
147
147
  end
148
148
 
@@ -152,22 +152,19 @@ module Lhm
152
152
 
153
153
  dest = @origin.destination_name
154
154
 
155
- if table?(dest)
155
+ if @connection.table_exists?(dest)
156
156
  error("#{ dest } should not exist; not cleaned up from previous run?")
157
157
  end
158
158
  end
159
159
 
160
160
  def execute
161
161
  destination_create
162
- sql(@statements)
162
+ @connection.sql(@statements)
163
163
  Migration.new(@origin, destination_read)
164
164
  end
165
165
 
166
166
  def destination_create
167
- original = "CREATE TABLE `#{ @origin.name }`"
168
- replacement = "CREATE TABLE `#{ @origin.destination_name }`"
169
-
170
- sql(@origin.ddl.gsub(original, replacement))
167
+ @connection.destination_create(@origin)
171
168
  end
172
169
 
173
170
  def destination_read
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
@@ -20,28 +20,10 @@ module Lhm
20
20
  end.join(', ')
21
21
  end
22
22
 
23
- def table?(table_name)
24
- connection.table_exists?(table_name)
25
- end
26
-
27
- def sql(statements)
28
- [statements].flatten.each do |statement|
29
- connection.execute(tagged(statement))
30
- end
31
- rescue ActiveRecord::StatementInvalid => e
32
- error e.message
33
- end
34
-
35
- def update(statements)
36
- [statements].flatten.inject(0) do |memo, statement|
37
- memo += connection.update(tagged(statement))
38
- end
39
- rescue ActiveRecord::StatementInvalid => e
40
- error e.message
41
- end
42
-
43
23
  def version_string
44
- connection.select_one("show variables like 'version'")["Value"]
24
+ row = connection.select_one("show variables like 'version'")
25
+ value = struct_key(row, "Value")
26
+ row[value]
45
27
  end
46
28
 
47
29
  private
@@ -81,5 +63,15 @@ module Lhm
81
63
  end
82
64
  return true
83
65
  end
66
+
67
+ def struct_key(struct, key)
68
+ keys = if struct.is_a? Hash
69
+ struct.keys
70
+ else
71
+ struct.members
72
+ end
73
+
74
+ keys.find {|k| k.to_s.downcase == key.to_s.downcase }
75
+ end
84
76
  end
85
77
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'lhm/sql_helper'
@@ -37,10 +37,7 @@ module Lhm
37
37
  end
38
38
 
39
39
  def ddl
40
- sql = "show create table `#{ @table_name }`"
41
- specification = nil
42
- @connection.execute(sql).each { |row| specification = row.last }
43
- specification
40
+ @connection.show_create(@table_name)
44
41
  end
45
42
 
46
43
  def parse
@@ -48,10 +45,14 @@ module Lhm
48
45
 
49
46
  Table.new(@table_name, extract_primary_key(schema), ddl).tap do |table|
50
47
  schema.each do |defn|
51
- table.columns[defn["COLUMN_NAME"]] = {
52
- :type => defn["COLUMN_TYPE"],
53
- :is_nullable => defn["IS_NULLABLE"],
54
- :column_default => defn["COLUMN_DEFAULT"]
48
+ column_name = struct_key(defn, "COLUMN_NAME")
49
+ column_type = struct_key(defn, "COLUMN_TYPE")
50
+ is_nullable = struct_key(defn, "IS_NULLABLE")
51
+ column_default = struct_key(defn, "COLUMN_DEFAULT")
52
+ table.columns[defn[column_name]] = {
53
+ :type => defn[column_type],
54
+ :is_nullable => defn[is_nullable],
55
+ :column_default => defn[column_default]
55
56
  }
56
57
  end
57
58
 
@@ -67,20 +68,25 @@ module Lhm
67
68
  @connection.select_all %Q{
68
69
  select *
69
70
  from information_schema.columns
70
- where table_name = "#{ @table_name }"
71
- and table_schema = "#{ @schema_name }"
71
+ where table_name = '#{ @table_name }'
72
+ and table_schema = '#{ @schema_name }'
72
73
  }
73
74
  end
74
75
 
75
76
  def read_indices
76
77
  @connection.select_all %Q{
77
78
  show indexes from `#{ @schema_name }`.`#{ @table_name }`
78
- where key_name != "PRIMARY"
79
+ where key_name != 'PRIMARY'
79
80
  }
80
81
  end
81
82
 
82
83
  def extract_indices(indices)
83
- indices.map { |row| [row["Key_name"], row["Column_name"]] }.
84
+ indices.
85
+ map do |row|
86
+ key_name = struct_key(row, "Key_name")
87
+ column_name = struct_key(row, "COLUMN_NAME")
88
+ [row[key_name], row[column_name]]
89
+ end.
84
90
  inject(Hash.new { |h, k| h[k] = []}) do |memo, (idx, column)|
85
91
  memo[idx] << column
86
92
  memo
@@ -88,8 +94,16 @@ module Lhm
88
94
  end
89
95
 
90
96
  def extract_primary_key(schema)
91
- cols = schema.select { |defn| defn["COLUMN_KEY"] == "PRI" }
92
- keys = cols.map { |defn| defn["COLUMN_NAME"] }
97
+ cols = schema.select do |defn|
98
+ column_key = struct_key(defn, "COLUMN_KEY")
99
+ defn[column_key] == "PRI"
100
+ end
101
+
102
+ keys = cols.map do |defn|
103
+ column_name = struct_key(defn, "COLUMN_NAME")
104
+ defn[column_name]
105
+ end
106
+
93
107
  keys.length == 1 ? keys.first : keys
94
108
  end
95
109
  end
@@ -1,6 +1,6 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
5
- VERSION = "1.1.0"
5
+ VERSION = "1.2.0"
6
6
  end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require 'minitest/spec'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
@@ -14,9 +14,9 @@ describe Lhm::AtomicSwitcher do
14
14
 
15
15
  describe "switching" do
16
16
  before(:each) do
17
- @origin = table_create("origin")
17
+ @origin = table_create("origin")
18
18
  @destination = table_create("destination")
19
- @migration = Lhm::Migration.new(@origin, @destination)
19
+ @migration = Lhm::Migration.new(@origin, @destination)
20
20
  end
21
21
 
22
22
  it "rename origin to archive" do
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
@@ -1,24 +1,29 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + "/../bootstrap"
5
5
 
6
- require 'active_record'
7
6
  begin
8
- require 'mysql2'
7
+ require 'active_record'
8
+ begin
9
+ require 'mysql2'
10
+ rescue LoadError
11
+ require 'mysql'
12
+ end
9
13
  rescue LoadError
10
- require 'mysql'
14
+ require 'dm-core'
15
+ require 'dm-mysql-adapter'
11
16
  end
12
17
  require 'lhm/table'
13
18
  require 'lhm/sql_helper'
19
+ require 'lhm/connection'
14
20
 
15
21
  module IntegrationHelper
16
22
  #
17
23
  # Connectivity
18
24
  #
19
-
20
25
  def connection
21
- ActiveRecord::Base.connection
26
+ @connection
22
27
  end
23
28
 
24
29
  def connect_master!
@@ -30,28 +35,37 @@ module IntegrationHelper
30
35
  end
31
36
 
32
37
  def connect!(port)
33
- ActiveRecord::Base.establish_connection(
34
- :adapter => defined?(Mysql2) ? 'mysql2' : 'mysql',
35
- :host => '127.0.0.1',
36
- :database => 'lhm',
37
- :username => '',
38
- :port => port
39
- )
38
+ adapter = nil
39
+ if defined?(ActiveRecord)
40
+ ActiveRecord::Base.establish_connection(
41
+ :adapter => defined?(Mysql2) ? 'mysql2' : 'mysql',
42
+ :host => '127.0.0.1',
43
+ :database => 'lhm',
44
+ :username => 'root',
45
+ :port => port
46
+ )
47
+ adapter = ActiveRecord::Base.connection
48
+ elsif defined?(DataMapper)
49
+ adapter = DataMapper.setup(:default, "mysql://root@localhost:#{port}/lhm")
50
+ end
51
+
52
+ Lhm.setup(adapter)
53
+ @connection = Lhm::Connection.new(adapter)
40
54
  end
41
55
 
42
56
  def select_one(*args)
43
- connection.select_one(*args)
57
+ @connection.select_one(*args)
44
58
  end
45
59
 
46
60
  def select_value(*args)
47
- connection.select_value(*args)
61
+ @connection.select_value(*args)
48
62
  end
49
63
 
50
64
  def execute(*args)
51
65
  retries = 10
52
66
  begin
53
- connection.execute(*args)
54
- rescue ActiveRecord::StatementInvalid => e
67
+ @connection.execute(*args)
68
+ rescue => e
55
69
  if (retries -= 1) > 0 && e.message =~ /Table '.*?' doesn't exist/
56
70
  sleep 0.1
57
71
  retry
@@ -69,8 +83,11 @@ module IntegrationHelper
69
83
  # check the master binlog position and wait for the slave to catch up
70
84
  # to that position.
71
85
  sleep 1
86
+ elsif
87
+ connect_master!
72
88
  end
73
89
 
90
+
74
91
  yield block
75
92
 
76
93
  if master_slave_mode?
@@ -93,7 +110,7 @@ module IntegrationHelper
93
110
  end
94
111
 
95
112
  def table_read(fixture_name)
96
- Lhm::Table.parse(fixture_name, connection)
113
+ Lhm::Table.parse(fixture_name, @connection)
97
114
  end
98
115
 
99
116
  def table_exists?(table)
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
@@ -161,10 +161,12 @@ describe Lhm do
161
161
 
162
162
  insert = Thread.new do
163
163
  10.times do |n|
164
+ connect_master!
164
165
  execute("insert into users set reference = '#{ 100 + n }'")
165
166
  sleep(0.17)
166
167
  end
167
168
  end
169
+ sleep 2
168
170
 
169
171
  options = { :stride => 10, :throttle => 97, :atomic_switch => false }
170
172
  Lhm.change_table(:users, options) do |t|
@@ -187,6 +189,7 @@ describe Lhm do
187
189
  sleep(0.17)
188
190
  end
189
191
  end
192
+ sleep 2
190
193
 
191
194
  options = { :stride => 10, :throttle => 97, :atomic_switch => false }
192
195
  Lhm.change_table(:users, options) do |t|
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
@@ -0,0 +1,40 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd.
2
+
3
+ require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
4
+ require 'lhm/connection'
5
+
6
+ if defined?(ActiveRecord)
7
+ describe Lhm::Connection::ActiveRecordConnection do
8
+ let(:active_record) { MiniTest::Mock.new }
9
+
10
+ before do
11
+ active_record.expect :current_database, 'the db'
12
+ end
13
+
14
+ after do
15
+ active_record.verify
16
+ end
17
+
18
+ it 'creates an ActiveRecord connection when the DM classes are not there' do
19
+ connection.must_be_instance_of(Lhm::Connection::ActiveRecordConnection)
20
+ end
21
+
22
+ it 'initializes the db name from the connection' do
23
+ connection.current_database.must_equal('the db')
24
+ end
25
+
26
+ it 'backticks the table names' do
27
+ table_name = 'my_table'
28
+
29
+ active_record.expect :execute,
30
+ [['returned sql']],
31
+ ["show create table `#{table_name}`"]
32
+
33
+ connection.show_create(table_name)
34
+ end
35
+
36
+ def connection
37
+ Lhm::Connection.new(active_record)
38
+ end
39
+ end
40
+ end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd.
2
+
3
+ require 'lhm/connection'
4
+
5
+ describe Lhm::Connection do
6
+ it 'raises an exception when it cannot find the dependencies' do
7
+ assert_raises(NameError) do
8
+ Connection.new({})
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd.
2
+
3
+ require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
4
+ require 'lhm/connection'
5
+
6
+ if defined?(DataMapper)
7
+ describe Lhm::Connection::DataMapperConnection do
8
+ let(:data_mapper) { MiniTest::Mock.new }
9
+ let(:options) { { 'database' => 'the db' } }
10
+
11
+ before do
12
+ data_mapper.expect :is_a?, true, [DataMapper::Adapters::AbstractAdapter]
13
+ data_mapper.expect :options, options
14
+ end
15
+
16
+ after do
17
+ data_mapper.verify
18
+ end
19
+
20
+ it 'creates a DataMapperConnection when the adapter is from DM' do
21
+ connection.must_be_instance_of(Lhm::Connection::DataMapperConnection)
22
+ end
23
+
24
+ it 'initializes the db name from the database option' do
25
+ connection.current_database.must_equal('the db')
26
+ end
27
+
28
+ it 'initializes the db name form the path if the database option is not available' do
29
+ options['database'] = nil
30
+ options['path'] = '/still the db'
31
+
32
+ connection.current_database.must_equal('still the db')
33
+ end
34
+
35
+ it 'backticks the table names' do
36
+ table_name = 'my_table'
37
+
38
+ data_mapper.expect :select,
39
+ [{ :sql => 'returned sql' }],
40
+ ["show create table `#{table_name}`"]
41
+
42
+ connection.show_create(table_name)
43
+ end
44
+
45
+ def connection
46
+ Lhm::Connection.new(data_mapper)
47
+ end
48
+ end
49
+ end
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
@@ -1,8 +1,19 @@
1
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + "/../bootstrap"
5
5
 
6
+ begin
7
+ require 'active_record'
8
+ begin
9
+ require 'mysql2'
10
+ rescue LoadError
11
+ require 'mysql'
12
+ end
13
+ rescue LoadError
14
+ require 'dm-core'
15
+ end
16
+
6
17
  module UnitHelper
7
18
  def fixture(name)
8
19
  File.read $fixtures.join(name)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lhm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2012-04-29 00:00:00.000000000 Z
15
+ date: 2013-02-22 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: minitest
@@ -46,22 +46,6 @@ dependencies:
46
46
  - - ! '>='
47
47
  - !ruby/object:Gem::Version
48
48
  version: '0'
49
- - !ruby/object:Gem::Dependency
50
- name: activerecord
51
- requirement: !ruby/object:Gem::Requirement
52
- none: false
53
- requirements:
54
- - - ! '>='
55
- - !ruby/object:Gem::Version
56
- version: '0'
57
- type: :runtime
58
- prerelease: false
59
- version_requirements: !ruby/object:Gem::Requirement
60
- none: false
61
- requirements:
62
- - - ! '>='
63
- - !ruby/object:Gem::Version
64
- version: '0'
65
49
  description: Migrate large tables without downtime by copying to a temporary table
66
50
  in chunks. The old table is not dropped. Instead, it is moved to timestamp_table_name
67
51
  for verification.
@@ -85,11 +69,13 @@ files:
85
69
  - gemfiles/ar-2.3_mysql.gemfile
86
70
  - gemfiles/ar-3.2_mysql.gemfile
87
71
  - gemfiles/ar-3.2_mysql2.gemfile
72
+ - gemfiles/dm_mysql.gemfile
88
73
  - lhm.gemspec
89
74
  - lib/lhm.rb
90
75
  - lib/lhm/atomic_switcher.rb
91
76
  - lib/lhm/chunker.rb
92
77
  - lib/lhm/command.rb
78
+ - lib/lhm/connection.rb
93
79
  - lib/lhm/entangler.rb
94
80
  - lib/lhm/intersection.rb
95
81
  - lib/lhm/invoker.rb
@@ -113,8 +99,11 @@ files:
113
99
  - spec/integration/lhm_spec.rb
114
100
  - spec/integration/locked_switcher_spec.rb
115
101
  - spec/integration/table_spec.rb
102
+ - spec/unit/active_record_connection_spec.rb
116
103
  - spec/unit/atomic_switcher_spec.rb
117
104
  - spec/unit/chunker_spec.rb
105
+ - spec/unit/connection_spec.rb
106
+ - spec/unit/datamapper_connection_spec.rb
118
107
  - spec/unit/entangler_spec.rb
119
108
  - spec/unit/intersection_spec.rb
120
109
  - spec/unit/locked_switcher_spec.rb
@@ -143,31 +132,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
132
  version: '0'
144
133
  requirements: []
145
134
  rubyforge_project:
146
- rubygems_version: 1.8.21
135
+ rubygems_version: 1.8.24
147
136
  signing_key:
148
137
  specification_version: 3
149
138
  summary: online schema changer for mysql
150
- test_files:
151
- - spec/README.md
152
- - spec/bootstrap.rb
153
- - spec/fixtures/destination.ddl
154
- - spec/fixtures/origin.ddl
155
- - spec/fixtures/small_table.ddl
156
- - spec/fixtures/users.ddl
157
- - spec/integration/atomic_switcher_spec.rb
158
- - spec/integration/chunker_spec.rb
159
- - spec/integration/entangler_spec.rb
160
- - spec/integration/integration_helper.rb
161
- - spec/integration/lhm_spec.rb
162
- - spec/integration/locked_switcher_spec.rb
163
- - spec/integration/table_spec.rb
164
- - spec/unit/atomic_switcher_spec.rb
165
- - spec/unit/chunker_spec.rb
166
- - spec/unit/entangler_spec.rb
167
- - spec/unit/intersection_spec.rb
168
- - spec/unit/locked_switcher_spec.rb
169
- - spec/unit/migration_spec.rb
170
- - spec/unit/migrator_spec.rb
171
- - spec/unit/sql_helper_spec.rb
172
- - spec/unit/table_spec.rb
173
- - spec/unit/unit_helper.rb
139
+ test_files: []