mysql_big_table_migration 0.1.2
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/.gitignore +5 -0
- data/.travis.yml +23 -0
- data/CHANGELOG +17 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +20 -0
- data/README.md +58 -0
- data/Rakefile +15 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/mysql_big_table_migration.rb +195 -0
- data/lib/mysql_big_table_migration/version.rb +3 -0
- data/mysql_big_table_migration.gemspec +36 -0
- data/rails/init.rb +2 -0
- data/tasks/mysql_big_table_migration_tasks.rake +4 -0
- data/test/database.yml +17 -0
- data/test/mysql_big_table_migration_test.rb +262 -0
- data/test/schema.rb +8 -0
- data/test/test_helper.rb +111 -0
- data/uninstall.rb +1 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2cf0a701c21368899da7dd1cb5ca1004a403513b
|
4
|
+
data.tar.gz: f2d290bdf85b48ba4140e360e730c7de86a50b1f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: caf05416fd733107981ddee51522b5c893c47d612da106525c5658503ee52ce1887a84d56b50b2bdc961ae714e479fea5a3c9bad9b78acb9c6e6631e440ad429
|
7
|
+
data.tar.gz: f19ea5701c1c20fd916368b5eb8b7e9fa84634340c2a03cb55de79e6df1a6b954a25490d487c6203a3a4f5597668e17fb787e59018aa3abdb9f7d8e87214f885
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
language: ruby
|
2
|
+
env:
|
3
|
+
- DB=mysql
|
4
|
+
rvm:
|
5
|
+
- 1.9
|
6
|
+
- 2.1
|
7
|
+
- 2.2
|
8
|
+
- rbx-2.5.2
|
9
|
+
bundler_args: --without production
|
10
|
+
before_script:
|
11
|
+
- mysql -e 'create database mysql_big_table_migration_test'
|
12
|
+
before_install:
|
13
|
+
- gem update --system
|
14
|
+
services:
|
15
|
+
- mysql
|
16
|
+
script:
|
17
|
+
- bundle install
|
18
|
+
- bundle exec rake test
|
19
|
+
cache:
|
20
|
+
- bundler
|
21
|
+
os:
|
22
|
+
- linux
|
23
|
+
- osx
|
data/CHANGELOG
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
TODO:
|
2
|
+
|
3
|
+
* Support change_table :foo do |t| style migrations (with option to activate temp table)
|
4
|
+
* Support ActiveRecord::Migration::CommandRecorder (rake db:rollback)
|
5
|
+
* Ensure index names based on original table
|
6
|
+
|
7
|
+
0.1.2:
|
8
|
+
|
9
|
+
* Moved repo from thickpaddy/mysql_big_table_migration to MakeYourLaws/mysql_big_table_migration
|
10
|
+
* Added dependencies on ActiveRecord & Rails, removed unused rdoc, updated to minitest, moved from Rails::VERSION to ActiveRecord::VERSION
|
11
|
+
* Fixed tests - mysql can't convert 'foo0' to an integer
|
12
|
+
* Moved dependencies into gemspec, updated Gemfile.lock
|
13
|
+
* Pushed to RubyGems
|
14
|
+
|
15
|
+
<= 0.1.1:
|
16
|
+
|
17
|
+
See https://github.com/thickpaddy/mysql_big_table_migration/commits/master
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
[](https://travis-ci.org/MakeYourLaws/mysql_big_table_migration)
|
2
|
+
|
3
|
+
MysqlBigTableMigration
|
4
|
+
======================
|
5
|
+
|
6
|
+
A Rails plugin that adds methods to ActiveRecord::Migration to allow columns
|
7
|
+
and indexes to be added to and removed from large tables with millions of
|
8
|
+
rows in MySQL, without leaving processes seemingly stalled in state "copy
|
9
|
+
to tmp table".
|
10
|
+
|
11
|
+
For each of the standard transformations that operate on columns or indexes,
|
12
|
+
this plugin adds a "using_tmp_table" version. These methods create a
|
13
|
+
temporary table with the same structure as the table to be altered, applies
|
14
|
+
the transformation to the temp table, copies data from the source table to
|
15
|
+
the temp table and then replaces the source table with the temporary one.
|
16
|
+
|
17
|
+
While it does try to ensure that data is consistent at the end of the entire
|
18
|
+
process by locking tables and looking for rows created or modified during
|
19
|
+
copying, this is NOT TRANSACTION SAFE as it (a) relies on timestamp columns
|
20
|
+
and (b) doesn't handle rows that have been deleted from the source table
|
21
|
+
after being copied to the temporary table.
|
22
|
+
|
23
|
+
Installation
|
24
|
+
============
|
25
|
+
|
26
|
+
source 'https://rubygems.org' do
|
27
|
+
...
|
28
|
+
gem 'mysql_big_table_migration'
|
29
|
+
...
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
Example
|
34
|
+
=======
|
35
|
+
|
36
|
+
|
37
|
+
class AddIndexOnSomeColumnToSomeTable < ActiveRecord::Migration
|
38
|
+
def self.up
|
39
|
+
add_index_using_tmp_table :some_table, :some_column
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.down
|
43
|
+
remove_index_using_tmp_table :some_table, :some_column
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
Copyright (c) 2010 Mark Woods, released under the MIT license
|
49
|
+
|
50
|
+
Testing
|
51
|
+
========
|
52
|
+
|
53
|
+
You will need to bundle install dependencies for the project, as well as create a test database. To install dependencies run `bundle install`. To create the database run the following in a MySQL prompt as an admin user:
|
54
|
+
|
55
|
+
CREATE DATABASE mysql_big_table_migration_test;
|
56
|
+
GRANT ALL PRIVILEGES ON *.* TO 'dev'@'localhost' IDENTIFIED BY 'password';
|
57
|
+
|
58
|
+
After bundling dependencies, and setting up the test database, run the tests with `rake test`
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
desc 'Default: run unit tests.'
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
desc 'Test the mysql_big_table_migration plugin.'
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.pattern = 'test/*_test.rb'
|
11
|
+
t.verbose = true
|
12
|
+
end
|
13
|
+
|
14
|
+
Bundler::GemHelper.install_tasks
|
15
|
+
Dir.glob('tasks/*.rake').each { |r| import r }
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'mysql_big_table_migration'
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module MySQLBigTableMigration
|
2
|
+
|
3
|
+
SUPPORTED_ADAPTERS = [
|
4
|
+
"ActiveRecord::ConnectionAdapters::MysqlAdapter",
|
5
|
+
"ActiveRecord::ConnectionAdapters::Mysql2Adapter"
|
6
|
+
]
|
7
|
+
|
8
|
+
def rename_column(table, old_name, new_name)
|
9
|
+
push_state unless @state_stack.try(:any?)
|
10
|
+
current_state[:renames] ||= {}
|
11
|
+
current_state[:renames][old_name] = new_name
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_column_using_tmp_table(table_name, *args)
|
16
|
+
with_tmp_table(table_name) do |tmp_table_name|
|
17
|
+
begin
|
18
|
+
add_column tmp_table_name, *args
|
19
|
+
rescue ActiveRecord::StatementInvalid => e
|
20
|
+
raise unless e.message.include?("Duplicate column name")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove_column_using_tmp_table(table_name, column_name)
|
26
|
+
with_tmp_table(table_name) do |tmp_table_name|
|
27
|
+
begin
|
28
|
+
remove_column tmp_table_name, column_name
|
29
|
+
rescue ActiveRecord::StatementInvalid => e
|
30
|
+
raise unless e.message.include?("check that column/key exists")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def rename_column_using_tmp_table(table_name, column_name, new_column_name)
|
36
|
+
with_tmp_table(table_name) { |tmp_table_name| rename_column(tmp_table_name, column_name, new_column_name) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def change_column_using_tmp_table(table_name, column_name, type, options={})
|
40
|
+
with_tmp_table(table_name) { |tmp_table_name| change_column(tmp_table_name, column_name, type, options) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_index_using_tmp_table(table_name, column_name, options={})
|
44
|
+
# generate the index name using the original table name if no name provided
|
45
|
+
options[:name] = index_name(table_name, :column => Array(column_name)) if options[:name].nil?
|
46
|
+
with_tmp_table(table_name) { |tmp_table_name| add_index(tmp_table_name, column_name, options) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def remove_index_using_tmp_table(table_name, options={})
|
50
|
+
with_tmp_table(table_name) { |tmp_table_name| remove_index(tmp_table_name, :name => index_name(table_name, options)) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def with_tmp_table(table_name)
|
54
|
+
raise ArgumentError, "block expected" unless block_given?
|
55
|
+
|
56
|
+
unless SUPPORTED_ADAPTERS.include? connection.class.name
|
57
|
+
puts "Warning: Unsupported connection adapter '#{connection.class.name}' for MySQL Big Table Migration Plugin"
|
58
|
+
puts " Migration methods will still be executed, but without using a temp table."
|
59
|
+
yield table_name
|
60
|
+
return
|
61
|
+
end
|
62
|
+
|
63
|
+
table_name = table_name.to_s
|
64
|
+
new_table_name = "tmp_new_" + table_name
|
65
|
+
old_table_name = "tmp_old_" + table_name
|
66
|
+
|
67
|
+
begin
|
68
|
+
say "Creating temporary table #{new_table_name} like #{table_name}..."
|
69
|
+
connection.execute("CREATE TABLE #{new_table_name} LIKE #{table_name}")
|
70
|
+
|
71
|
+
# yield the temporary table name to the block, which should alter the table using standard migration methods
|
72
|
+
push_state
|
73
|
+
yield new_table_name
|
74
|
+
state = pop_state
|
75
|
+
|
76
|
+
rename_columns ||= state[:renames] || {}
|
77
|
+
|
78
|
+
# get column names to copy *after* yielding to block - could drop a column from new table
|
79
|
+
# note: do not get column names using the column_names method, we need to make sure we avoid obtaining a cached array of column names
|
80
|
+
old_column_names = []
|
81
|
+
each_result_hash(connection.execute("DESCRIBE #{table_name}")) { |row| old_column_names << row['Field'] } # see ruby mysql docs for more info
|
82
|
+
new_column_names = []
|
83
|
+
each_result_hash(connection.execute("DESCRIBE #{new_table_name}")) { |row| new_column_names << row['Field'] }
|
84
|
+
|
85
|
+
shared_columns = old_column_names & new_column_names
|
86
|
+
|
87
|
+
old_column_list = column_list(shared_columns + rename_columns.keys)
|
88
|
+
new_column_list = column_list(shared_columns + rename_columns.values)
|
89
|
+
|
90
|
+
timestamp_before_migration = fetch_result_row(connection.execute("SELECT CURRENT_TIMESTAMP"))[0] # note: string, not time object
|
91
|
+
max_id_before_migration = fetch_result_row(connection.execute("SELECT MAX(id) FROM #{table_name}"))[0].to_i
|
92
|
+
|
93
|
+
if max_id_before_migration == 0
|
94
|
+
say "Source table is empty, no rows to copy into temporary table"
|
95
|
+
else
|
96
|
+
batch_size = mysql_big_table_migration_bach_size
|
97
|
+
start = fetch_result_row(connection.execute("SELECT MIN(id) FROM #{table_name}"))[0].to_i
|
98
|
+
counter = start
|
99
|
+
say "Inserting into temporary table in batches of #{batch_size}..."
|
100
|
+
say "Approximately #{max_id_before_migration-start+1} rows to process, first row has id #{start}", true
|
101
|
+
while counter <= ( max = fetch_result_row(connection.execute("SELECT MAX(id) FROM #{table_name}"))[0].to_i )
|
102
|
+
percentage_complete = mysql_big_table_migration_completion start, counter, max
|
103
|
+
say "Processing rows with ids between #{counter} and #{(counter+batch_size)-1} (#{percentage_complete}% complete)", true
|
104
|
+
connection.execute("INSERT INTO #{new_table_name} (#{new_column_list}) SELECT #{old_column_list} FROM #{table_name} WHERE id >= #{counter} AND id < #{counter + batch_size}")
|
105
|
+
counter = counter + batch_size
|
106
|
+
end
|
107
|
+
say "Finished inserting into temporary table"
|
108
|
+
end
|
109
|
+
|
110
|
+
rescue Exception => e
|
111
|
+
drop_table new_table_name
|
112
|
+
raise
|
113
|
+
end
|
114
|
+
|
115
|
+
say "Replacing source table with temporary table..."
|
116
|
+
rename_table table_name, old_table_name
|
117
|
+
rename_table new_table_name, table_name
|
118
|
+
|
119
|
+
say "Cleaning up, checking for rows created/updated during migration, dropping old table..."
|
120
|
+
begin
|
121
|
+
connection.execute("LOCK TABLES #{table_name} WRITE, #{old_table_name} READ")
|
122
|
+
recently_created_or_updated_conditions = "id > #{max_id_before_migration}"
|
123
|
+
recently_created_or_updated_conditions << " OR updated_at > '#{timestamp_before_migration}'" if old_column_names.include?("updated_at")
|
124
|
+
connection.execute("REPLACE INTO #{table_name} (#{new_column_list}) SELECT #{old_column_list} FROM #{old_table_name} WHERE #{recently_created_or_updated_conditions}")
|
125
|
+
rescue Exception => e
|
126
|
+
puts "Failed to lock tables and do final cleanup. This may not be anything to worry about, especially on an infrequently used table."
|
127
|
+
puts "ERROR MESSAGE: " + e.message
|
128
|
+
ensure
|
129
|
+
connection.execute("UNLOCK TABLES")
|
130
|
+
end
|
131
|
+
drop_table old_table_name
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def mysql_big_table_migration_completion(start, counter, max)
|
137
|
+
number_done = counter - start + 1
|
138
|
+
number_to_do = max - start + 1
|
139
|
+
(number_done * 100 / number_to_do.to_f).to_i
|
140
|
+
end
|
141
|
+
|
142
|
+
def mysql_big_table_migration_bach_size
|
143
|
+
10000
|
144
|
+
end
|
145
|
+
|
146
|
+
def connection
|
147
|
+
ActiveRecord::Base.connection
|
148
|
+
end
|
149
|
+
|
150
|
+
def each_result_hash(result, &block)
|
151
|
+
case connection.class.name
|
152
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
153
|
+
result.each_hash(&block)
|
154
|
+
when "ActiveRecord::ConnectionAdapters::Mysql2Adapter"
|
155
|
+
result.each(:as => :hash, &block)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def fetch_result_row(result)
|
160
|
+
case connection.class.name
|
161
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
162
|
+
result.fetch_row
|
163
|
+
when "ActiveRecord::ConnectionAdapters::Mysql2Adapter"
|
164
|
+
result.first
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def column_list(column_names)
|
169
|
+
"`" + column_names.join("`, `") + "`"
|
170
|
+
end
|
171
|
+
|
172
|
+
def push_state
|
173
|
+
@state_stack ||= []
|
174
|
+
@state_stack.push({})
|
175
|
+
end
|
176
|
+
|
177
|
+
def pop_state
|
178
|
+
@state_stack.pop
|
179
|
+
end
|
180
|
+
|
181
|
+
def current_state
|
182
|
+
@state_stack.last
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
if Object.const_defined?("ActiveRecord")
|
188
|
+
class ActiveRecord::Migration
|
189
|
+
if ActiveRecord::VERSION::STRING < "3.0"
|
190
|
+
extend MySQLBigTableMigration
|
191
|
+
else
|
192
|
+
include MySQLBigTableMigration
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
$LOAD_PATH.unshift 'lib'
|
2
|
+
require "mysql_big_table_migration/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "mysql_big_table_migration"
|
6
|
+
s.version = MySQLBigTableMigration::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.date = Time.now.strftime('%Y-%m-%d')
|
9
|
+
s.summary = "allow columns and indexes to be added to and removed from large tables"
|
10
|
+
s.homepage = "http://github.com/MakeYourLaws/mysql_big_table_migration"
|
11
|
+
s.email = "sai@makeyourlaws.org"
|
12
|
+
s.authors = [ "Mark Woods" ]
|
13
|
+
s.has_rdoc = false
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test}/*`.split("\n")
|
17
|
+
# s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency "activerecord"
|
21
|
+
s.add_dependency "activesupport"
|
22
|
+
s.add_development_dependency "bundler"
|
23
|
+
s.add_development_dependency "fileutils"
|
24
|
+
s.add_development_dependency "logger"
|
25
|
+
s.add_development_dependency "minitest"
|
26
|
+
s.add_development_dependency "mysql"
|
27
|
+
s.add_development_dependency "mysql2"
|
28
|
+
s.add_development_dependency "rake"
|
29
|
+
|
30
|
+
s.description = <<desc
|
31
|
+
A Rails plugin that adds methods to ActiveRecord::Migration to allow columns
|
32
|
+
and indexes to be added to and removed from large tables with millions of
|
33
|
+
rows in MySQL, without leaving processes seemingly stalled in state "copy
|
34
|
+
to tmp table".
|
35
|
+
desc
|
36
|
+
end
|
data/rails/init.rb
ADDED
data/test/database.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
basics: &basics
|
2
|
+
username: travis
|
3
|
+
database: mysql_big_table_migration_test
|
4
|
+
socket: <%= [
|
5
|
+
'/var/lib/mysql/mysql.sock',
|
6
|
+
'/var/run/mysqld/mysqld.sock',
|
7
|
+
'/tmp/mysqld.sock',
|
8
|
+
'/tmp/mysql.sock',
|
9
|
+
].detect { |socket| File.exist?(socket) } %>
|
10
|
+
|
11
|
+
mysql:
|
12
|
+
<<: *basics
|
13
|
+
adapter: mysql
|
14
|
+
|
15
|
+
mysql2:
|
16
|
+
<<: *basics
|
17
|
+
adapter: mysql2
|
@@ -0,0 +1,262 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MysqlBigTableMigrationTest < Minitest::Test
|
4
|
+
extend DatabaseTest
|
5
|
+
|
6
|
+
test_against_all_configs :methods_are_added_to_migration do
|
7
|
+
if ActiveRecord::VERSION::STRING < "3.0"
|
8
|
+
method_target = ActiveRecord::Migration
|
9
|
+
else
|
10
|
+
method_target = ActiveRecord::Migration.new
|
11
|
+
end
|
12
|
+
|
13
|
+
MySQLBigTableMigration.instance_methods(false).each do |method|
|
14
|
+
assert_respond_to method_target, method
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
test_against_all_configs :with_tmp_table_creates_tmp_table do
|
19
|
+
silence_stream($stdout) do
|
20
|
+
ActiveRecord::Migration.send(:with_tmp_table, :test_table) {}
|
21
|
+
end
|
22
|
+
assert_match "CREATE TABLE tmp_new_test_table LIKE test_table", read_log_file
|
23
|
+
end
|
24
|
+
|
25
|
+
class SmallBatchMigration < ActiveRecord::Migration
|
26
|
+
def mysql_big_table_migration_bach_size
|
27
|
+
4
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
test_against_all_configs :with_tmp_table_copies_all_rows do
|
32
|
+
silence_stream($stdout) do
|
33
|
+
SmallBatchMigration.new.send(:with_tmp_table, :test_table) {}
|
34
|
+
end
|
35
|
+
|
36
|
+
assert_equal 5, test_table_rows.length
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
test_against_all_configs :with_exactly_one_row, fixture_row_count: 1 do
|
41
|
+
silence_stream($stdout) do
|
42
|
+
ActiveRecord::Migration.send(:with_tmp_table, :test_table) {}
|
43
|
+
end
|
44
|
+
|
45
|
+
assert_equal 1, test_table_rows.length
|
46
|
+
end
|
47
|
+
|
48
|
+
test_against_all_configs :add_column_using_tmp_table do
|
49
|
+
silence_stream($stdout) do
|
50
|
+
ActiveRecord::Migration.add_column_using_tmp_table(:test_table, :baz, :string)
|
51
|
+
end
|
52
|
+
|
53
|
+
fields = test_table_fields
|
54
|
+
assert_equal 4, fields.length
|
55
|
+
assert_equal "baz", fields[3]["Field"]
|
56
|
+
assert_equal "varchar(255)", fields[3]["Type"]
|
57
|
+
|
58
|
+
results = test_table_rows
|
59
|
+
assert_equal 5, results.length
|
60
|
+
assert_equal "foo2", results[2]["foo"]
|
61
|
+
assert_equal "bar3", results[3]["bar"]
|
62
|
+
assert_equal nil, results[4]["baz"]
|
63
|
+
end
|
64
|
+
|
65
|
+
test_against_all_configs :remove_column_using_tmp_table do
|
66
|
+
silence_stream($stdout) do
|
67
|
+
ActiveRecord::Migration.remove_column_using_tmp_table(:test_table, :bar)
|
68
|
+
end
|
69
|
+
|
70
|
+
fields = test_table_fields
|
71
|
+
assert_equal 2, fields.length
|
72
|
+
assert_equal "id", fields[0]["Field"]
|
73
|
+
assert_equal "foo", fields[1]["Field"]
|
74
|
+
|
75
|
+
results = test_table_rows
|
76
|
+
assert_equal 5, results.length
|
77
|
+
assert_equal "foo2", results[2]["foo"]
|
78
|
+
assert !results[3].has_key?("bar")
|
79
|
+
end
|
80
|
+
|
81
|
+
test_against_all_configs :rename_column_using_tmp_table do
|
82
|
+
silence_stream($stdout) do
|
83
|
+
ActiveRecord::Migration.rename_column_using_tmp_table(:test_table, :foo, :baz)
|
84
|
+
end
|
85
|
+
|
86
|
+
fields = test_table_fields
|
87
|
+
assert_equal 3, fields.length
|
88
|
+
assert_equal "id", fields[0]["Field"]
|
89
|
+
assert_equal "int(11)", fields[0]["Type"]
|
90
|
+
assert_equal "baz", fields[1]["Field"]
|
91
|
+
assert_equal "varchar(255)", fields[1]["Type"]
|
92
|
+
assert_equal "bar", fields[2]["Field"]
|
93
|
+
assert_equal "varchar(255)", fields[2]["Type"]
|
94
|
+
|
95
|
+
results = test_table_rows
|
96
|
+
assert_equal 5, results.length
|
97
|
+
5.times do |i|
|
98
|
+
assert_equal "foo#{i}", results[i]["baz"]
|
99
|
+
assert_equal "bar#{i}", results[i]["bar"]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
test_against_all_configs :change_column_using_tmp_table do
|
104
|
+
silence_stream($stdout) do
|
105
|
+
ActiveRecord::Migration.change_column_using_tmp_table(:test_table, :bar, :text)
|
106
|
+
end
|
107
|
+
|
108
|
+
fields = test_table_fields
|
109
|
+
assert_equal 3, fields.length
|
110
|
+
assert_equal "id", fields[0]["Field"]
|
111
|
+
assert_equal "foo", fields[1]["Field"]
|
112
|
+
assert_equal "bar", fields[2]["Field"]
|
113
|
+
assert_equal "text", fields[2]["Type"]
|
114
|
+
|
115
|
+
results = test_table_rows
|
116
|
+
assert_equal 5, results.length
|
117
|
+
assert_equal "foo2", results[2]["foo"]
|
118
|
+
assert_equal "bar3", results[3]["bar"]
|
119
|
+
end
|
120
|
+
|
121
|
+
test_against_all_configs :add_index_using_tmp_table do
|
122
|
+
silence_stream($stdout) do
|
123
|
+
ActiveRecord::Migration.add_index_using_tmp_table(:test_table, :bar)
|
124
|
+
end
|
125
|
+
|
126
|
+
indexes = result_hashes("SHOW INDEX FROM test_table")
|
127
|
+
assert_equal 3, indexes.length
|
128
|
+
assert_equal "id", indexes[0]["Column_name"]
|
129
|
+
assert_equal "foo", indexes[1]["Column_name"]
|
130
|
+
assert_equal "bar", indexes[2]["Column_name"]
|
131
|
+
end
|
132
|
+
|
133
|
+
test_against_all_configs :remove_index_using_tmp_table do
|
134
|
+
silence_stream($stdout) do
|
135
|
+
ActiveRecord::Migration.remove_index_using_tmp_table(:test_table, :foo)
|
136
|
+
end
|
137
|
+
|
138
|
+
indexes = result_hashes("SHOW INDEX FROM test_table")
|
139
|
+
assert_equal 1, indexes.length
|
140
|
+
assert_equal "id", indexes[0]["Column_name"]
|
141
|
+
end
|
142
|
+
|
143
|
+
test_against_all_configs :rename_with_remove do
|
144
|
+
silence_stream($stdout) do
|
145
|
+
ActiveRecord::Migration.with_tmp_table(:test_table) do |tmp_table_name|
|
146
|
+
ActiveRecord::Migration.rename_column tmp_table_name, :bar, :baz
|
147
|
+
ActiveRecord::Migration.remove_column tmp_table_name, :foo
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
fields = test_table_fields
|
152
|
+
assert_equal 2, fields.length
|
153
|
+
assert_equal "id", fields[0]["Field"]
|
154
|
+
assert_equal "baz", fields[1]["Field"]
|
155
|
+
|
156
|
+
results = test_table_rows
|
157
|
+
assert_equal 5, results.length
|
158
|
+
5.times do |i|
|
159
|
+
assert_equal "bar#{i}", results[i]["baz"]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
test_against_all_configs :rename_with_add do
|
164
|
+
silence_stream($stdout) do
|
165
|
+
ActiveRecord::Migration.with_tmp_table(:test_table) do |tmp_table_name|
|
166
|
+
ActiveRecord::Migration.rename_column tmp_table_name, :bar, :baz
|
167
|
+
ActiveRecord::Migration.add_column tmp_table_name, :dummy, :integer
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
fields = test_table_fields
|
172
|
+
assert_equal 4, fields.length
|
173
|
+
assert_equal "id", fields[0]["Field"]
|
174
|
+
assert_equal "foo", fields[1]["Field"]
|
175
|
+
assert_equal "baz", fields[2]["Field"]
|
176
|
+
assert_equal "dummy", fields[3]["Field"]
|
177
|
+
|
178
|
+
results = test_table_rows
|
179
|
+
assert_equal 5, results.length
|
180
|
+
5.times do |i|
|
181
|
+
assert_equal "foo#{i}", results[i]["foo"]
|
182
|
+
assert_equal "bar#{i}", results[i]["baz"]
|
183
|
+
assert_equal nil, results[i]["dummy"]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
test_against_all_configs :rename_with_change do
|
188
|
+
silence_stream($stdout) do
|
189
|
+
ActiveRecord::Migration.with_tmp_table(:test_table) do |tmp_table_name|
|
190
|
+
ActiveRecord::Migration.rename_column tmp_table_name, :bar, :baz
|
191
|
+
# MySQL can't properly change string data to integer ("Incorrect integer value: 'foo0'")
|
192
|
+
# Also, limit: 3 will fail ("Data too long") w/ sql_mode = 'STRICT_ALL_TABLES'
|
193
|
+
ActiveRecord::Migration.change_column tmp_table_name, :foo, :string, limit: 10
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
fields = test_table_fields
|
198
|
+
assert_equal 3, fields.length
|
199
|
+
assert_equal "id", fields[0]["Field"]
|
200
|
+
assert_equal "foo", fields[1]["Field"]
|
201
|
+
assert_equal "varchar(10)", fields[1]["Type"]
|
202
|
+
assert_equal "baz", fields[2]["Field"]
|
203
|
+
|
204
|
+
results = test_table_rows
|
205
|
+
assert_equal 5, results.length
|
206
|
+
5.times do |i|
|
207
|
+
assert_equal "foo#{i}", results[i]["foo"]
|
208
|
+
assert_equal "bar#{i}", results[i]["baz"]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
test_against_all_configs :rename_with_rename do
|
213
|
+
silence_stream($stdout) do
|
214
|
+
ActiveRecord::Migration.with_tmp_table(:test_table) do |tmp_table_name|
|
215
|
+
ActiveRecord::Migration.rename_column tmp_table_name, :bar, :baz
|
216
|
+
ActiveRecord::Migration.rename_column tmp_table_name, :foo, :dummy
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
fields = test_table_fields
|
221
|
+
assert_equal 3, fields.length
|
222
|
+
assert_equal "id", fields[0]["Field"]
|
223
|
+
assert_equal "dummy", fields[1]["Field"]
|
224
|
+
assert_equal "baz", fields[2]["Field"]
|
225
|
+
|
226
|
+
results = test_table_rows
|
227
|
+
assert_equal 5, results.length
|
228
|
+
5.times do |i|
|
229
|
+
assert_equal "foo#{i}", results[i]["dummy"]
|
230
|
+
assert_equal "bar#{i}", results[i]["baz"]
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
test_against_all_configs :rename_column do
|
235
|
+
silence_stream($stdout) do
|
236
|
+
ActiveRecord::Migration.rename_column :test_table, :bar, :baz
|
237
|
+
end
|
238
|
+
|
239
|
+
fields = test_table_fields
|
240
|
+
assert_equal 3, fields.length
|
241
|
+
assert_equal "id", fields[0]["Field"]
|
242
|
+
assert_equal "foo", fields[1]["Field"]
|
243
|
+
assert_equal "baz", fields[2]["Field"]
|
244
|
+
|
245
|
+
results = test_table_rows
|
246
|
+
assert_equal 5, results.length
|
247
|
+
5.times do |i|
|
248
|
+
assert_equal "foo#{i}", results[i]["foo"]
|
249
|
+
assert_equal "bar#{i}", results[i]["baz"]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
def test_table_fields
|
256
|
+
result_hashes("DESCRIBE test_table")
|
257
|
+
end
|
258
|
+
|
259
|
+
def test_table_rows
|
260
|
+
result_hashes("SELECT * FROM test_table")
|
261
|
+
end
|
262
|
+
end
|
data/test/schema.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'active_record'
|
5
|
+
require 'active_record/connection_adapters/mysql_adapter'
|
6
|
+
require 'active_support'
|
7
|
+
require 'active_support/core_ext'
|
8
|
+
require 'mysql'
|
9
|
+
require 'mysql2'
|
10
|
+
require 'logger'
|
11
|
+
require 'yaml'
|
12
|
+
require File.dirname(__FILE__) + "/../lib/mysql_big_table_migration"
|
13
|
+
|
14
|
+
TEST_CONFIGS = ["mysql", "mysql2"]
|
15
|
+
|
16
|
+
Mysql::Result.class_eval do
|
17
|
+
unless respond_to?(:all_hashes)
|
18
|
+
def all_hashes
|
19
|
+
rows = []
|
20
|
+
each_hash do |row|
|
21
|
+
rows << row
|
22
|
+
end
|
23
|
+
rows
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def read_log_file
|
29
|
+
@log.string
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_schema(adapter)
|
33
|
+
config = YAML.load(ERB.new(File.read(File.join(File.dirname(__FILE__), 'database.yml'))).result)
|
34
|
+
@log = StringIO.new
|
35
|
+
ActiveRecord::Base.logger = Logger.new(@log)
|
36
|
+
ActiveRecord::Base.establish_connection(config[adapter.to_s])
|
37
|
+
load(File.join(File.dirname(__FILE__), "schema.rb"))
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_fixtures(options = {})
|
41
|
+
connection = ActiveRecord::Base.connection
|
42
|
+
|
43
|
+
connection.execute("DELETE FROM test_table;")
|
44
|
+
(options[:fixture_row_count] || 5).times do |i|
|
45
|
+
connection.execute("INSERT INTO test_table (foo, bar) VALUES ('foo#{i}', 'bar#{i}');")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def assert_valid_database_setup(options = {})
|
50
|
+
fields = result_hashes("DESCRIBE test_table")
|
51
|
+
assert_equal 3, fields.length
|
52
|
+
assert_equal "id", fields[0]["Field"]
|
53
|
+
assert_equal "int(11)", fields[0]["Type"]
|
54
|
+
assert_equal "foo", fields[1]["Field"]
|
55
|
+
assert_equal "varchar(255)", fields[1]["Type"]
|
56
|
+
assert_equal "bar", fields[2]["Field"]
|
57
|
+
assert_equal "varchar(255)", fields[2]["Type"]
|
58
|
+
|
59
|
+
indexes = result_hashes("SHOW INDEX FROM test_table")
|
60
|
+
assert_equal 2, indexes.length
|
61
|
+
assert_equal "id", indexes[0]["Column_name"]
|
62
|
+
assert_equal "foo", indexes[1]["Column_name"]
|
63
|
+
|
64
|
+
results = result_hashes("SELECT * FROM test_table")
|
65
|
+
assert_equal options[:fixture_row_count] || 5, results.length
|
66
|
+
end
|
67
|
+
|
68
|
+
def result_hashes(query)
|
69
|
+
result = connection.execute(query)
|
70
|
+
case connection
|
71
|
+
when ActiveRecord::ConnectionAdapters::MysqlAdapter
|
72
|
+
result.all_hashes
|
73
|
+
when ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
74
|
+
result.each(:as => :hash)
|
75
|
+
else
|
76
|
+
raise "Unknown adapter"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Proc#bind removed from ActiveSupport 4+
|
81
|
+
class Proc
|
82
|
+
def bind(object)
|
83
|
+
block, time = self, Time.now
|
84
|
+
object.class_eval do
|
85
|
+
method_name = "__bind_#{time.to_i}_#{time.usec}"
|
86
|
+
define_method(method_name, &block)
|
87
|
+
method = instance_method(method_name)
|
88
|
+
remove_method(method_name)
|
89
|
+
method
|
90
|
+
end.bind(object)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
module DatabaseTest
|
95
|
+
def test_against_all_configs(name, options = {}, &block)
|
96
|
+
TEST_CONFIGS.each do |config|
|
97
|
+
self.send(:define_method, :"test_#{name.to_s}_with_#{config}") do
|
98
|
+
silence_stream($stdout) do
|
99
|
+
load_schema(config)
|
100
|
+
load_fixtures options
|
101
|
+
end
|
102
|
+
assert_valid_database_setup options
|
103
|
+
block.bind(self).call
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def connection
|
110
|
+
ActiveRecord::Base.connection
|
111
|
+
end
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
metadata
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mysql_big_table_migration
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark Woods
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
type: :runtime
|
15
|
+
name: activerecord
|
16
|
+
prerelease: false
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
type: :runtime
|
29
|
+
name: activesupport
|
30
|
+
prerelease: false
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
type: :development
|
43
|
+
name: bundler
|
44
|
+
prerelease: false
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
type: :development
|
57
|
+
name: fileutils
|
58
|
+
prerelease: false
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
type: :development
|
71
|
+
name: logger
|
72
|
+
prerelease: false
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
type: :development
|
85
|
+
name: minitest
|
86
|
+
prerelease: false
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
type: :development
|
99
|
+
name: mysql
|
100
|
+
prerelease: false
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
type: :development
|
113
|
+
name: mysql2
|
114
|
+
prerelease: false
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
type: :development
|
127
|
+
name: rake
|
128
|
+
prerelease: false
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: |2
|
140
|
+
A Rails plugin that adds methods to ActiveRecord::Migration to allow columns
|
141
|
+
and indexes to be added to and removed from large tables with millions of
|
142
|
+
rows in MySQL, without leaving processes seemingly stalled in state "copy
|
143
|
+
to tmp table".
|
144
|
+
email: sai@makeyourlaws.org
|
145
|
+
executables: []
|
146
|
+
extensions: []
|
147
|
+
extra_rdoc_files: []
|
148
|
+
files:
|
149
|
+
- ".gitignore"
|
150
|
+
- ".travis.yml"
|
151
|
+
- CHANGELOG
|
152
|
+
- Gemfile
|
153
|
+
- MIT-LICENSE
|
154
|
+
- README.md
|
155
|
+
- Rakefile
|
156
|
+
- init.rb
|
157
|
+
- install.rb
|
158
|
+
- lib/mysql_big_table_migration.rb
|
159
|
+
- lib/mysql_big_table_migration/version.rb
|
160
|
+
- mysql_big_table_migration.gemspec
|
161
|
+
- rails/init.rb
|
162
|
+
- tasks/mysql_big_table_migration_tasks.rake
|
163
|
+
- test/database.yml
|
164
|
+
- test/mysql_big_table_migration_test.rb
|
165
|
+
- test/schema.rb
|
166
|
+
- test/test_helper.rb
|
167
|
+
- uninstall.rb
|
168
|
+
homepage: http://github.com/MakeYourLaws/mysql_big_table_migration
|
169
|
+
licenses: []
|
170
|
+
metadata: {}
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
requirements: []
|
186
|
+
rubyforge_project:
|
187
|
+
rubygems_version: 2.4.6
|
188
|
+
signing_key:
|
189
|
+
specification_version: 4
|
190
|
+
summary: allow columns and indexes to be added to and removed from large tables
|
191
|
+
test_files: []
|