strong_migrations 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +138 -0
- data/Rakefile +10 -0
- data/lib/strong_migrations.rb +6 -0
- data/lib/strong_migrations/migration.rb +135 -0
- data/lib/strong_migrations/unsafe_migration.rb +4 -0
- data/lib/strong_migrations/version.rb +3 -0
- data/strong_migrations.gemspec +26 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a0871a07d2e13749e0f984373c303dd837d7ea95
|
4
|
+
data.tar.gz: 146fa1034aa8c8c8d5094b0bffa5079e728e6c54
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4743a779ecede6c6fca241a2f7c0eee01e46c24c79b5af071f850fb4ced23c852f9390f49afbe97323dd247d1dd0a378a3001f0ed6ba57839a34ce3e29ab9b0c
|
7
|
+
data.tar.gz: 8ed8682c0b22ba7c0f7881b9036e6c6e02dc40ffd7f112c08403c0276cbc986f8672e4f3b759ddaeb2b2dd4fc6b7825923917e078c9861a4f887713098cf005c
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.2
|
4
|
+
gemfile:
|
5
|
+
- Gemfile
|
6
|
+
- test/gemfiles/activerecord32.gemfile
|
7
|
+
- test/gemfiles/activerecord40.gemfile
|
8
|
+
- test/gemfiles/activerecord41.gemfile
|
9
|
+
script: bundle exec rake test
|
10
|
+
before_script:
|
11
|
+
- psql -c 'create database strong_migrations_test;' -U postgres
|
12
|
+
notifications:
|
13
|
+
email:
|
14
|
+
on_success: never
|
15
|
+
on_failure: change
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Bob Remeika and David Waller, 2015 Andrew Kane
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# Strong Migrations
|
2
|
+
|
3
|
+
Catch unsafe migrations at dev time
|
4
|
+
|
5
|
+
[](https://travis-ci.org/ankane/strong_migrations)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application’s Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'strong_migrations'
|
13
|
+
```
|
14
|
+
|
15
|
+
## Dangerous Operations
|
16
|
+
|
17
|
+
- adding an index non-concurrently
|
18
|
+
- adding a column with a non-null default value
|
19
|
+
- changing the type of a column
|
20
|
+
- renaming a table
|
21
|
+
- renaming a column
|
22
|
+
- removing a column
|
23
|
+
- adding a `json` column (Postgres only)
|
24
|
+
|
25
|
+
For more info, check out:
|
26
|
+
|
27
|
+
- [Rails Migrations with No Downtime](http://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
|
28
|
+
- [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/) (if it’s relevant)
|
29
|
+
|
30
|
+
## The Zero Downtime Way
|
31
|
+
|
32
|
+
### Adding an index
|
33
|
+
|
34
|
+
Add indexes concurrently.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration
|
38
|
+
def change
|
39
|
+
commit_db_transaction
|
40
|
+
add_index :users, :some_index, algorithm: :concurrently
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
### Adding a column with a default value
|
46
|
+
|
47
|
+
1. Add the column without a default value
|
48
|
+
2. Commit the transaction
|
49
|
+
3. Backfill the column
|
50
|
+
4. Add the default value
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration
|
54
|
+
def up
|
55
|
+
# 1
|
56
|
+
add_column :users, :some_column, :text
|
57
|
+
|
58
|
+
# 2
|
59
|
+
commit_db_transaction
|
60
|
+
|
61
|
+
# 3
|
62
|
+
User.find_in_batches do |users|
|
63
|
+
User.where(id: users.map(&:id)).update_all some_column: "default_value"
|
64
|
+
end
|
65
|
+
|
66
|
+
# 4
|
67
|
+
change_column_default :users, :some_column, "default_value"
|
68
|
+
end
|
69
|
+
|
70
|
+
def down
|
71
|
+
remove_column :users, :some_column
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
### Renaming or changing the type of a column
|
77
|
+
|
78
|
+
There’s no way to do this without downtime.
|
79
|
+
|
80
|
+
If you really have to:
|
81
|
+
|
82
|
+
1. Create a new column
|
83
|
+
2. Write to both columns
|
84
|
+
3. Backfill data from the old column to the new column
|
85
|
+
4. Move reads from the old column to the new column
|
86
|
+
5. Stop writing to the old column
|
87
|
+
6. Drop the old column
|
88
|
+
|
89
|
+
### Renaming a table
|
90
|
+
|
91
|
+
Same as renaming a column - see above.
|
92
|
+
|
93
|
+
### Removing a column
|
94
|
+
|
95
|
+
Tell ActiveRecord to [ignore the column](http://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/) from its cache.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
class User
|
99
|
+
def self.columns
|
100
|
+
super.reject { |c| c.name == "some_column" }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
Once it’s deployed, create a migration to remove the column.
|
106
|
+
|
107
|
+
### Adding a json column
|
108
|
+
|
109
|
+
There’s no equality operator for the `json` column type. Replace all calls to `uniq` with a custom scope.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
scope :uniq_on_id, -> { select("DISTINCT ON (your_table.id) your_table.*") }
|
113
|
+
```
|
114
|
+
|
115
|
+
## Assuring Safety
|
116
|
+
|
117
|
+
To mark a step in the migration as safe, despite using method that might otherwise be dangerous, wrap it in a `safety_assured` block.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class MySafeMigration < ActiveRecord::Migration
|
121
|
+
def change
|
122
|
+
safety_assured { remove_column :users, :some_column }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
## Credits
|
128
|
+
|
129
|
+
Thanks to Bob Remeika and David Waller for the [original code](https://github.com/foobarfighter/safe-migrations).
|
130
|
+
|
131
|
+
## Contributing
|
132
|
+
|
133
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
134
|
+
|
135
|
+
- [Report bugs](https://github.com/ankane/strong_migrations/issues)
|
136
|
+
- Fix bugs and [submit pull requests](https://github.com/ankane/strong_migrations/pulls)
|
137
|
+
- Write, clarify, or fix documentation
|
138
|
+
- Suggest or add new features
|
data/Rakefile
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
module StrongMigrations
|
2
|
+
module Migration
|
3
|
+
def safety_assured
|
4
|
+
previous_value = @safe
|
5
|
+
@safe = true
|
6
|
+
yield
|
7
|
+
ensure
|
8
|
+
@safe = previous_value
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(method, *args, &block)
|
12
|
+
unless @safe || is_a?(ActiveRecord::Schema)
|
13
|
+
case method
|
14
|
+
when :remove_column
|
15
|
+
raise_error :remove_column
|
16
|
+
when :remove_timestamps
|
17
|
+
raise_error :remove_column
|
18
|
+
when :change_table
|
19
|
+
raise_error :change_table
|
20
|
+
when :rename_table
|
21
|
+
raise_error :rename_table
|
22
|
+
when :rename_column
|
23
|
+
raise_error :rename_column
|
24
|
+
when :add_index
|
25
|
+
options = args[2]
|
26
|
+
unless options && options[:algorithm] == :concurrently
|
27
|
+
raise_error :add_index
|
28
|
+
end
|
29
|
+
when :add_column
|
30
|
+
type = args[2]
|
31
|
+
options = args[3]
|
32
|
+
raise_error :add_column_default if options && !options[:default].nil?
|
33
|
+
raise_error :add_column_json if type.to_s == "json"
|
34
|
+
when :change_column
|
35
|
+
raise_error :change_column
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def raise_error(message_key)
|
45
|
+
message =
|
46
|
+
case message_key
|
47
|
+
when :add_column_default
|
48
|
+
"Adding a column with a non-null default requires
|
49
|
+
the entire table and indexes to be rewritten. Instead:
|
50
|
+
|
51
|
+
1. Add the column without a default value
|
52
|
+
2. Commit the transaction
|
53
|
+
3. Backfill the column
|
54
|
+
4. Add the default value"
|
55
|
+
when :add_column_json
|
56
|
+
"There's no equality operator for the json column type.
|
57
|
+
Replace all calls to uniq with a custom scope.
|
58
|
+
|
59
|
+
scope :uniq_on_id, -> { select(\"DISTINCT ON (your_table.id) your_table.*\") }
|
60
|
+
|
61
|
+
Once it's deployed, wrap this step in a safety_assured { ... } block."
|
62
|
+
when :change_column
|
63
|
+
"Changing the type of an existing column requires
|
64
|
+
the entire table and indexes to be rewritten.
|
65
|
+
|
66
|
+
If you really have to:
|
67
|
+
|
68
|
+
1. Create a new column
|
69
|
+
2. Write to both columns
|
70
|
+
3. Backfill data from the old column to the new column
|
71
|
+
4. Move reads from the old column to the new column
|
72
|
+
5. Stop writing to the old column
|
73
|
+
6. Drop the old column"
|
74
|
+
when :remove_column
|
75
|
+
"ActiveRecord caches attributes which causes problems
|
76
|
+
when removing columns. Be sure to ignored the column:
|
77
|
+
|
78
|
+
class User
|
79
|
+
def self.columns
|
80
|
+
super.reject { |c| c.name == \"some_column\" }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
Once it's deployed, wrap this step in a safety_assured { ... } block."
|
85
|
+
when :rename_column
|
86
|
+
"There's no way to rename a column without downtime.
|
87
|
+
|
88
|
+
If you really have to:
|
89
|
+
|
90
|
+
1. Create a new column
|
91
|
+
2. Write to both columns
|
92
|
+
3. Backfill data from the old column to new column
|
93
|
+
4. Move reads from the old column to the new column
|
94
|
+
5. Stop writing to the old column
|
95
|
+
6. Drop the old column"
|
96
|
+
when :rename_table
|
97
|
+
"There's no way to rename a table without downtime.
|
98
|
+
|
99
|
+
If you really have to:
|
100
|
+
|
101
|
+
1. Create a new table
|
102
|
+
2. Write to both tables
|
103
|
+
3. Backfill data from the old table to new table
|
104
|
+
4. Move reads from the old table to the new table
|
105
|
+
5. Stop writing to the old table
|
106
|
+
6. Drop the old table"
|
107
|
+
when :add_index
|
108
|
+
"Adding a non-concurrent index locks the table. Instead, use:
|
109
|
+
|
110
|
+
def change
|
111
|
+
commit_db_transaction
|
112
|
+
add_index :users, :some_column, algorithm: :concurrently
|
113
|
+
end"
|
114
|
+
when :change_table
|
115
|
+
"The strong_migrations gem does not support inspecting what happens inside a
|
116
|
+
change_table block, so cannot help you here. Please make really sure that what
|
117
|
+
you're doing is safe before proceding, then wrap it in a safety_assured { ... } block."
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
message.prepend '
|
122
|
+
__ __ _____ _______ _
|
123
|
+
\ \ / /\ |_ _|__ __| |
|
124
|
+
\ \ /\ / / \ | | | | | |
|
125
|
+
\ \/ \/ / /\ \ | | | | | |
|
126
|
+
\ /\ / ____ \ _| |_ | | |_|
|
127
|
+
\/ \/_/ \_\_____| |_| (_)
|
128
|
+
|
129
|
+
'
|
130
|
+
message << "\n"
|
131
|
+
|
132
|
+
raise StrongMigrations::UnsafeMigration, message
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "strong_migrations/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "strong_migrations"
|
8
|
+
spec.version = StrongMigrations::VERSION
|
9
|
+
spec.authors = ["Bob Remeika", "David Waller", "Andrew Kane"]
|
10
|
+
spec.email = ["bob.remeika@gmail.com", "andrew@chartkick.com"]
|
11
|
+
|
12
|
+
spec.summary = "Catch unsafe migrations at dev time"
|
13
|
+
spec.homepage = "https://github.com/ankane/strong_migrations"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "activerecord", ">= 3.2.0"
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "minitest"
|
25
|
+
spec.add_development_dependency "pg"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: strong_migrations
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bob Remeika
|
8
|
+
- David Waller
|
9
|
+
- Andrew Kane
|
10
|
+
autorequire:
|
11
|
+
bindir: exe
|
12
|
+
cert_chain: []
|
13
|
+
date: 2015-11-23 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: 3.2.0
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: bundler
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - "~>"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '1.10'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '1.10'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: rake
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '10.0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - "~>"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '10.0'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: minitest
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
type: :development
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: pg
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
description:
|
86
|
+
email:
|
87
|
+
- bob.remeika@gmail.com
|
88
|
+
- andrew@chartkick.com
|
89
|
+
executables: []
|
90
|
+
extensions: []
|
91
|
+
extra_rdoc_files: []
|
92
|
+
files:
|
93
|
+
- ".gitignore"
|
94
|
+
- ".travis.yml"
|
95
|
+
- Gemfile
|
96
|
+
- LICENSE.txt
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- lib/strong_migrations.rb
|
100
|
+
- lib/strong_migrations/migration.rb
|
101
|
+
- lib/strong_migrations/unsafe_migration.rb
|
102
|
+
- lib/strong_migrations/version.rb
|
103
|
+
- strong_migrations.gemspec
|
104
|
+
homepage: https://github.com/ankane/strong_migrations
|
105
|
+
licenses: []
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.4.5.1
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: Catch unsafe migrations at dev time
|
127
|
+
test_files: []
|