mysql_online_migrations 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +21 -17
- data/lib/mysql_online_migrations/columns.rb +15 -0
- data/mysql_online_migrations.gemspec +5 -4
- data/spec/lib/mysql_online_migrations/columns_spec.rb +71 -1
- data/spec/lib/mysql_online_migrations/indexes_spec.rb +1 -0
- data/spec/support/helpers.rb +10 -2
- data/spec/support/shared_examples/mysql_online_migrations.rb +15 -3
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1577a541999a6a5ba4675641f7ce752819a79253
|
4
|
+
data.tar.gz: fa2c478e55d72bc8d78e386a36f493fe98d58f30
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24384b66dfa4347dffa0c38469937eea5097ea2b1a90b4dbf64fc2be96f1f6b52ce95204c0035e68150503a422fbfab5ad0bed2244368ec0f8d0e3bf8cecd02e
|
7
|
+
data.tar.gz: 4ec8b0d6cf8558b4a1e1c36d6f7bd46eaabc805d2226acc793593b9722c4d9de94c0bb64b9a51e744eedb2163a872dea2c6e525002c17954fef19baa68326d71
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,21 +1,22 @@
|
|
1
1
|
mysql_online_migrations
|
2
2
|
=======================
|
3
3
|
|
4
|
-
Patch Rails migrations to
|
5
|
-
Prior to MySQL 5.6, when adding / removing / renaming indexes and columns, MySQL would lock the writes of the whole table.
|
6
|
-
MySQL 5.6
|
4
|
+
Patch Rails migrations to enforce MySQL 5.6 online migrations.
|
5
|
+
Prior to MySQL 5.6, when adding / removing / renaming indexes and columns, MySQL would lock the writes of the whole table.
|
6
|
+
MySQL 5.6 by default will try to apply the least locking possible. You however don't know what kind of locking it applies and there's situations where it can't allow writes during a migration (See Caveats).
|
7
|
+
This gem enforces `LOCK=NONE` in all migration statements of Rails. Therefore, you're getting an error when MySQL cannot write during the migration so there's no surprise when rolling out in production.
|
7
8
|
|
8
9
|
|
9
10
|
Requirements
|
10
11
|
=======================
|
11
|
-
Built for Rails 3.2.15, may be compatible with Rails 4.0 but you'd lose the new features introduced.
|
12
|
+
Built for Rails 3.2.15, may be compatible with Rails 4.0 but you'd lose the new features introduced.
|
12
13
|
This gem actually just requires ActiveRecord and ActiveSupport, the full Rails gem should not be required.
|
13
14
|
|
14
15
|
List of requirements :
|
15
16
|
|
16
17
|
- Use mysql2 adapter
|
17
18
|
- Use ActiveRecord "~> 3.2.15"
|
18
|
-
- Use MySQL or Percona Server 5.6.X
|
19
|
+
- Use MySQL or Percona Server 5.6.X with InnoDB
|
19
20
|
|
20
21
|
Scope of this gem
|
21
22
|
=======================
|
@@ -25,36 +26,39 @@ Patch Rails migrations to automatically add `LOCK=NONE` in the following context
|
|
25
26
|
- Index management : `add_index`, `remove_index`, `rename_index`
|
26
27
|
- Add column : `add_column`, `add_timestamps`
|
27
28
|
- Remove column : `remove_column`, `remove_timestamps`
|
29
|
+
- Change column : `change_column`, `change_column_null`, `change_column_default`
|
28
30
|
|
29
31
|
Usage
|
30
32
|
=======================
|
31
|
-
In a typical Rails app, just add it to your
|
33
|
+
In a typical Rails app, just add it to your Gemfile :
|
34
|
+
|
32
35
|
`gem 'mysql_online_migrations'`
|
33
36
|
|
34
|
-
|
37
|
+
Then run `bundle install`
|
38
|
+
|
39
|
+
You're ready for online migrations! Please read the caveats section though.
|
35
40
|
|
36
41
|
### Turn it off for a whole environment
|
37
|
-
Example for environment test (your CI might not use MySQL 5.6 yet), add the following to `config/environments/test.rb`:
|
42
|
+
Example for environment test (your CI might not use MySQL 5.6 yet), add the following to `config/environments/test.rb`:
|
38
43
|
`config.active_record.mysql_online_migrations = false`
|
39
44
|
|
40
45
|
### Turn it off for a specific statement
|
41
|
-
Add `lock: true` to any of the method calls mentioned above. Example :
|
46
|
+
Add `lock: true` to any of the method calls mentioned above. Example :
|
42
47
|
`add_index :users, :name, lock: true`
|
43
48
|
|
44
|
-
The `lock:
|
49
|
+
The `lock: true` will be useful when hitting the caveats of `LOCK=NONE`. Please read the following section.
|
45
50
|
|
46
51
|
Caveats
|
47
52
|
=======================
|
48
53
|
|
49
|
-
|
54
|
+
The MySQL manual contains a list of which DDL statements can be run with `LOCK=NONE` under [Table 14.5 Summary of Online Status for DDL Operations](http://dev.mysql.com/doc/refman/5.6/en/innodb-create-index-overview.html). The short version is that __you can not yet__:
|
50
55
|
|
51
56
|
- Index a column of type text
|
52
57
|
- Change the type of a column
|
53
58
|
- Change the length of a column
|
54
|
-
-
|
55
|
-
-
|
56
|
-
- Other stuff found on https://blogs.oracle.com/mysqlinnodb/entry/online_alter_table_in_mysql :
|
57
|
-
- when the table contains FULLTEXT indexes or a hidden FTS_DOC_ID column, or
|
58
|
-
- when there are FOREIGN KEY constraints referring to the table, with ON…CASCADE or ON…SET NULL option.
|
59
|
+
- Set a column to NOT NULL (at least not with the default SQL_MODE)
|
60
|
+
- Adding an AUTO_INCREMENT column,
|
59
61
|
|
60
|
-
If you don't use `lock: true` when
|
62
|
+
If you don't use `lock: true` when online migration is not supported, you'll get a MySQL exception. No risk to lock the table by accident.
|
63
|
+
It's therefore highly recommended to use it in development/test/staging environment before running migrations in production.
|
64
|
+
If you have to perform such a migration without locking the table, tools such as [pt-online-schema-change](http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html) and [LHM](https://github.com/soundcloud/lhm) are viable options
|
@@ -15,6 +15,21 @@ module MysqlOnlineMigrations
|
|
15
15
|
execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)} #{lock_statement(lock, true)}")
|
16
16
|
end
|
17
17
|
|
18
|
+
def change_column_default(table_name, column_name, default, options = {})
|
19
|
+
column = column_for(table_name, column_name)
|
20
|
+
change_column table_name, column_name, column.sql_type, options.merge(:default => default)
|
21
|
+
end
|
22
|
+
|
23
|
+
def change_column_null(table_name, column_name, null, default = nil, options = {})
|
24
|
+
column = column_for(table_name, column_name)
|
25
|
+
|
26
|
+
unless null || default.nil?
|
27
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
28
|
+
end
|
29
|
+
|
30
|
+
change_column table_name, column_name, column.sql_type, options.merge(:null => null)
|
31
|
+
end
|
32
|
+
|
18
33
|
def rename_column(table_name, column_name, new_column_name, options = {})
|
19
34
|
lock, options = extract_lock_from_options(options)
|
20
35
|
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)} #{lock_statement(lock, true)}")
|
@@ -1,8 +1,8 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'mysql_online_migrations'
|
3
|
-
s.version = '0.1.
|
4
|
-
s.summary = "Use MySQL 5.6+ capacities to
|
5
|
-
s.description = "MySQL 5.6 adds a
|
3
|
+
s.version = '0.1.4'
|
4
|
+
s.summary = "Use MySQL 5.6+ capacities to enforce online migrations"
|
5
|
+
s.description = "MySQL 5.6 adds a `LOCK=NONE` option to make sure migrations are done with no locking. Let's use it."
|
6
6
|
s.authors = ["Anthony Alberto"]
|
7
7
|
s.email = 'alberto.anthony@gmail.com'
|
8
8
|
s.homepage = 'https://github.com/anthonyalberto/mysql_online_migrations'
|
@@ -18,4 +18,5 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
|
-
|
21
|
+
s.license = 'MIT'
|
22
|
+
end
|
@@ -2,6 +2,7 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe MysqlOnlineMigrations::Columns do
|
4
4
|
let(:comma_before_lock_none) { true }
|
5
|
+
let(:queries_with_lock) { {} }
|
5
6
|
|
6
7
|
context "#add_column" do
|
7
8
|
let(:queries) do
|
@@ -81,10 +82,31 @@ describe MysqlOnlineMigrations::Columns do
|
|
81
82
|
|
82
83
|
context "#change_column" do
|
83
84
|
let(:queries) do
|
84
|
-
# Unsupported with lock=none : change column type, change limit,
|
85
|
+
# Unsupported with lock=none : change column type, change limit, set NOT NULL.
|
85
86
|
{
|
86
87
|
[:testing, :foo, :string, { default: 'def', limit: 100 }] =>
|
87
88
|
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(100) DEFAULT 'def'",
|
89
|
+
[:testing, :foo, :string, { null: true, limit: 100 }] =>
|
90
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(100) DEFAULT NULL",
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
let(:queries_with_lock) do
|
95
|
+
{
|
96
|
+
[:testing, :foo, :string, { limit: 200 }] =>
|
97
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(200) DEFAULT NULL",
|
98
|
+
[:testing, :foo, :string, { default: 'def' }] =>
|
99
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(255) DEFAULT 'def'",
|
100
|
+
[:testing, :foo, :string, { null: false }] =>
|
101
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(255) NOT NULL",
|
102
|
+
[:testing, :foo, :string, { null: false, default: 'def', limit: 200 }] =>
|
103
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(200) DEFAULT 'def' NOT NULL",
|
104
|
+
[:testing, :foo, :string, { null: true }] =>
|
105
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(255) DEFAULT NULL",
|
106
|
+
[:testing, :foo, :integer, { null: true, limit: 6 }] =>
|
107
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` bigint DEFAULT NULL",
|
108
|
+
[:testing, :foo, :integer, { null: true, limit: 1 }] =>
|
109
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` tinyint DEFAULT NULL"
|
88
110
|
}
|
89
111
|
end
|
90
112
|
|
@@ -92,6 +114,54 @@ describe MysqlOnlineMigrations::Columns do
|
|
92
114
|
|
93
115
|
it_behaves_like "a method that adds LOCK=NONE when needed"
|
94
116
|
it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
|
117
|
+
it_behaves_like "queries_with_lock that executes properly"
|
118
|
+
end
|
119
|
+
|
120
|
+
context "#change_column_default" do
|
121
|
+
let(:queries) do
|
122
|
+
{
|
123
|
+
[:testing, :foo, 'def', {}] =>
|
124
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(100) DEFAULT 'def'",
|
125
|
+
[:testing, :foo, nil, {}] =>
|
126
|
+
"ALTER TABLE `testing` CHANGE `foo` `foo` varchar(100) DEFAULT NULL"
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
let(:method_name) { :change_column_default }
|
131
|
+
|
132
|
+
it_behaves_like "a method that adds LOCK=NONE when needed"
|
133
|
+
it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
|
134
|
+
it_behaves_like "queries_with_lock that executes properly"
|
135
|
+
end
|
136
|
+
|
137
|
+
context "#change_column_null" do
|
138
|
+
let(:queries) do
|
139
|
+
#change_column_null doesn't set DEFAULT in sql. It just issues an update statement before setting the NULL value if setting NULL to false
|
140
|
+
{
|
141
|
+
[:testing, :bam, true, nil, {}] =>
|
142
|
+
"ALTER TABLE `testing` CHANGE `bam` `bam` varchar(100) DEFAULT 'test'",
|
143
|
+
[:testing, :bam, true, 'def', {}] =>
|
144
|
+
"ALTER TABLE `testing` CHANGE `bam` `bam` varchar(100) DEFAULT 'test'"
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
let(:queries_with_lock) do
|
149
|
+
{
|
150
|
+
[:testing, :bam, false, nil, {}] =>
|
151
|
+
"ALTER TABLE `testing` CHANGE `bam` `bam` varchar(100) DEFAULT 'test' NOT NULL",
|
152
|
+
[:testing, :bam, false, 'def', {}] =>
|
153
|
+
[
|
154
|
+
"UPDATE `testing` SET `bam`='def' WHERE `bam` IS NULL",
|
155
|
+
"ALTER TABLE `testing` CHANGE `bam` `bam` varchar(100) DEFAULT 'test' NOT NULL"
|
156
|
+
]
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
let(:method_name) { :change_column_null }
|
161
|
+
|
162
|
+
it_behaves_like "a method that adds LOCK=NONE when needed"
|
163
|
+
it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
|
164
|
+
it_behaves_like "queries_with_lock that executes properly"
|
95
165
|
end
|
96
166
|
|
97
167
|
context "#rename_column" do
|
data/spec/support/helpers.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
module Helpers
|
2
|
+
CATCH_STATEMENT_REGEX = /^(alter|create|drop|update) /i
|
3
|
+
DDL_STATEMENT_REGEX = /^(alter|create|drop) /i
|
4
|
+
|
2
5
|
def execute(statement)
|
3
6
|
end
|
4
7
|
|
@@ -9,7 +12,7 @@ module Helpers
|
|
9
12
|
def stub_execute
|
10
13
|
original_execute = @adapter.method(:execute)
|
11
14
|
@adapter.stub(:execute) do |statement|
|
12
|
-
if statement =~
|
15
|
+
if statement =~ CATCH_STATEMENT_REGEX
|
13
16
|
execute(statement.squeeze(' ').strip)
|
14
17
|
else
|
15
18
|
original_execute.call(statement)
|
@@ -18,7 +21,11 @@ module Helpers
|
|
18
21
|
end
|
19
22
|
|
20
23
|
def add_lock_none(str, with_comma)
|
21
|
-
|
24
|
+
if str =~ DDL_STATEMENT_REGEX
|
25
|
+
"#{str}#{with_comma ? ' ,' : ''} LOCK=NONE"
|
26
|
+
else
|
27
|
+
str
|
28
|
+
end
|
22
29
|
end
|
23
30
|
|
24
31
|
def rebuild_table
|
@@ -28,6 +35,7 @@ module Helpers
|
|
28
35
|
t.column :foo, :string, :limit => 100
|
29
36
|
t.column :bar, :string, :limit => 100
|
30
37
|
t.column :baz, :string, :limit => 100
|
38
|
+
t.column :bam, :string, :limit => 100, default: "test", null: false
|
31
39
|
t.column :extra, :string, :limit => 100
|
32
40
|
t.timestamps
|
33
41
|
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
shared_examples_for "a method that adds LOCK=NONE when needed" do
|
2
2
|
before(:each) do
|
3
3
|
stub_execute
|
4
|
+
@queries = queries.merge(queries_with_lock)
|
4
5
|
end
|
5
6
|
|
6
7
|
it "adds LOCK=NONE at the end of the query" do
|
7
|
-
queries.each do |arguments, output|
|
8
|
+
@queries.each do |arguments, output|
|
8
9
|
Array.wrap(output).each { |out| should_receive(:execute).with(add_lock_none(out, comma_before_lock_none)) }
|
9
10
|
@adapter.public_send(method_name, *arguments)
|
10
11
|
end
|
@@ -14,7 +15,7 @@ shared_examples_for "a method that adds LOCK=NONE when needed" do
|
|
14
15
|
before(:each) { set_ar_setting(false) }
|
15
16
|
|
16
17
|
it "doesn't add lock none" do
|
17
|
-
queries.each do |arguments, output|
|
18
|
+
@queries.each do |arguments, output|
|
18
19
|
Array.wrap(output).each { |out| should_receive(:execute).with(out) }
|
19
20
|
@adapter.public_send(method_name, *arguments)
|
20
21
|
end
|
@@ -23,7 +24,7 @@ shared_examples_for "a method that adds LOCK=NONE when needed" do
|
|
23
24
|
|
24
25
|
context "with lock: true option" do
|
25
26
|
it "doesn't add lock none" do
|
26
|
-
queries.each do |arguments, output|
|
27
|
+
@queries.each do |arguments, output|
|
27
28
|
Array.wrap(output).each { |out| should_receive(:execute).with(out) }
|
28
29
|
arguments[-1] = arguments.last.merge(lock: true)
|
29
30
|
@adapter.public_send(method_name, *arguments)
|
@@ -41,4 +42,15 @@ shared_examples_for "a request with LOCK=NONE that doesn't crash in MySQL" do
|
|
41
42
|
end
|
42
43
|
end
|
43
44
|
end
|
45
|
+
end
|
46
|
+
|
47
|
+
shared_examples_for "queries_with_lock that executes properly" do
|
48
|
+
it "succeeds without exception" do
|
49
|
+
queries.each do |_, output|
|
50
|
+
Array.wrap(output).each do |out|
|
51
|
+
@adapter.execute(out)
|
52
|
+
rebuild_table
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
44
56
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mysql_online_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anthony Alberto
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-11-
|
11
|
+
date: 2013-11-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -94,8 +94,8 @@ dependencies:
|
|
94
94
|
- - '>='
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
-
description: MySQL 5.6 adds a
|
98
|
-
|
97
|
+
description: MySQL 5.6 adds a `LOCK=NONE` option to make sure migrations are done
|
98
|
+
with no locking. Let's use it.
|
99
99
|
email: alberto.anthony@gmail.com
|
100
100
|
executables: []
|
101
101
|
extensions: []
|
@@ -117,7 +117,8 @@ files:
|
|
117
117
|
- spec/support/helpers.rb
|
118
118
|
- spec/support/shared_examples/mysql_online_migrations.rb
|
119
119
|
homepage: https://github.com/anthonyalberto/mysql_online_migrations
|
120
|
-
licenses:
|
120
|
+
licenses:
|
121
|
+
- MIT
|
121
122
|
metadata: {}
|
122
123
|
post_install_message:
|
123
124
|
rdoc_options: []
|
@@ -138,7 +139,7 @@ rubyforge_project:
|
|
138
139
|
rubygems_version: 2.0.2
|
139
140
|
signing_key:
|
140
141
|
specification_version: 4
|
141
|
-
summary: Use MySQL 5.6+ capacities to
|
142
|
+
summary: Use MySQL 5.6+ capacities to enforce online migrations
|
142
143
|
test_files:
|
143
144
|
- spec/lib/mysql_online_migrations/columns_spec.rb
|
144
145
|
- spec/lib/mysql_online_migrations/indexes_spec.rb
|