strong_migrations 0.1.9 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +59 -23
- data/lib/strong_migrations.rb +116 -1
- data/lib/strong_migrations/migration.rb +1 -92
- data/lib/strong_migrations/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b381f6d77227aa434bfae57e8bd63b5ee74181a
|
4
|
+
data.tar.gz: f1049edf7a7b01f3c075c0401003de048b7bde90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28d568d129cb527e9e5465f8c583ef7cab4972da9e9142a944a899753b22f8e90346f3c1a9d63c1c3ea32b939b058716aa6734030ebca352a1a8cc507e3bd565
|
7
|
+
data.tar.gz: 7c8bc5d03567a87fab67ec089c9a0ca71d2704079b5a2960530e7b97d3f76e78fd772a949d5e36b19582eea49024cdd2a7e7e6a8062344ae673a70a52be41d96
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -4,7 +4,7 @@ Catch unsafe migrations at dev time
|
|
4
4
|
|
5
5
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
6
6
|
|
7
|
-
[![Build Status](https://travis-ci.org/ankane/strong_migrations.svg)](https://travis-ci.org/ankane/strong_migrations)
|
7
|
+
[![Build Status](https://travis-ci.org/ankane/strong_migrations.svg?branch=master)](https://travis-ci.org/ankane/strong_migrations)
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
@@ -16,12 +16,13 @@ gem 'strong_migrations'
|
|
16
16
|
|
17
17
|
## Dangerous Operations
|
18
18
|
|
19
|
+
The following operations can cause downtime or errors:
|
20
|
+
|
19
21
|
- adding a column with a non-null default value to an existing table
|
20
22
|
- changing the type of a column
|
21
23
|
- renaming a table
|
22
24
|
- renaming a column
|
23
25
|
- removing a column
|
24
|
-
- executing arbitrary SQL
|
25
26
|
- adding an index non-concurrently (Postgres only)
|
26
27
|
- adding a `json` column to an existing table (Postgres only)
|
27
28
|
|
@@ -39,9 +40,9 @@ Also checks for best practices:
|
|
39
40
|
### Adding a column with a default value
|
40
41
|
|
41
42
|
1. Add the column without a default value
|
42
|
-
2.
|
43
|
-
3.
|
44
|
-
4.
|
43
|
+
2. Add the default value
|
44
|
+
3. Commit the transaction - **extremely important if you are backfilling in the migration**
|
45
|
+
4. Backfill the column
|
45
46
|
|
46
47
|
```ruby
|
47
48
|
class AddSomeColumnToUsers < ActiveRecord::Migration
|
@@ -50,18 +51,18 @@ class AddSomeColumnToUsers < ActiveRecord::Migration
|
|
50
51
|
add_column :users, :some_column, :text
|
51
52
|
|
52
53
|
# 2
|
54
|
+
change_column_default :users, :some_column, "default_value"
|
55
|
+
|
56
|
+
# 3
|
53
57
|
commit_db_transaction
|
54
58
|
|
55
|
-
#
|
59
|
+
# 4.a (Rails 5+)
|
56
60
|
User.in_batches.update_all some_column: "default_value"
|
57
61
|
|
58
|
-
#
|
62
|
+
# 4.b (Rails < 5)
|
59
63
|
User.find_in_batches do |users|
|
60
64
|
User.where(id: users.map(&:id)).update_all some_column: "default_value"
|
61
65
|
end
|
62
|
-
|
63
|
-
# 4
|
64
|
-
change_column_default :users, :some_column, "default_value"
|
65
66
|
end
|
66
67
|
|
67
68
|
def down
|
@@ -94,23 +95,36 @@ If you really have to:
|
|
94
95
|
|
95
96
|
### Removing a column
|
96
97
|
|
97
|
-
|
98
|
+
ActiveRecord caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots. To prevent this:
|
98
99
|
|
99
|
-
|
100
|
-
# For Rails 5+
|
101
|
-
class User < ActiveRecord::Base
|
102
|
-
self.ignored_columns = %w(some_column)
|
103
|
-
end
|
100
|
+
1. Tell ActiveRecord to ignore the column from its cache
|
104
101
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
102
|
+
```ruby
|
103
|
+
# For Rails 5+
|
104
|
+
class User < ApplicationRecord
|
105
|
+
self.ignored_columns = %w(some_column)
|
109
106
|
end
|
110
|
-
end
|
111
|
-
```
|
112
107
|
|
113
|
-
|
108
|
+
# For Rails < 5
|
109
|
+
class User < ActiveRecord::Base
|
110
|
+
def self.columns
|
111
|
+
super.reject { |c| c.name == "some_column" }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
2. Deploy code
|
117
|
+
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration
|
121
|
+
def change
|
122
|
+
safety_assured { remove_column :users, :some_column }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
4. Deploy and run migration
|
114
128
|
|
115
129
|
### Adding an index (Postgres)
|
116
130
|
|
@@ -153,6 +167,8 @@ To mark migrations as safe that were created before installing this gem, create
|
|
153
167
|
StrongMigrations.start_after = 20170101000000
|
154
168
|
```
|
155
169
|
|
170
|
+
Use the version from your latest migration.
|
171
|
+
|
156
172
|
## Dangerous Tasks
|
157
173
|
|
158
174
|
For safety, dangerous rake tasks are disabled in production - `db:drop`, `db:reset`, `db:schema:load`, and `db:structure:load`. To get around this, use:
|
@@ -178,6 +194,16 @@ Columns can flip order in `db/schema.rb` when you have multiple developers. One
|
|
178
194
|
task "db:schema:dump": "strong_migrations:alphabetize_columns"
|
179
195
|
```
|
180
196
|
|
197
|
+
## Custom Error Messages
|
198
|
+
|
199
|
+
To customize specific error messages, create an initializer with:
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
StrongMigrations.error_messages[:add_column_default] = "Your custom instructions"
|
203
|
+
```
|
204
|
+
|
205
|
+
Check the source code for the list of keys.
|
206
|
+
|
181
207
|
## Analyze Tables (Postgres)
|
182
208
|
|
183
209
|
Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with:
|
@@ -186,6 +212,16 @@ Analyze tables automatically (to update planner statistics) after an index is ad
|
|
186
212
|
StrongMigrations.auto_analyze = true
|
187
213
|
```
|
188
214
|
|
215
|
+
## Lock Timeout (Postgres)
|
216
|
+
|
217
|
+
It’s a good idea to set a lock timeout for the database user that runs migrations. This way, if migrations can’t acquire a lock in a timely manner, other statements won’t be stuck behind it.
|
218
|
+
|
219
|
+
```sql
|
220
|
+
ALTER ROLE myuser SET lock_timeout = '10s';
|
221
|
+
```
|
222
|
+
|
223
|
+
There’s also [a gem](https://github.com/gocardless/activerecord-safer_migrations) you can use for this.
|
224
|
+
|
189
225
|
## Credits
|
190
226
|
|
191
227
|
Thanks to Bob Remeika and David Waller for the [original code](https://github.com/foobarfighter/safe-migrations).
|
data/lib/strong_migrations.rb
CHANGED
@@ -6,10 +6,125 @@ require "strong_migrations/railtie" if defined?(Rails)
|
|
6
6
|
|
7
7
|
module StrongMigrations
|
8
8
|
class << self
|
9
|
-
attr_accessor :auto_analyze, :start_after
|
9
|
+
attr_accessor :auto_analyze, :start_after, :error_messages
|
10
10
|
end
|
11
11
|
self.auto_analyze = false
|
12
12
|
self.start_after = 0
|
13
|
+
self.error_messages = {
|
14
|
+
add_column_default:
|
15
|
+
"Adding a column with a non-null default requires
|
16
|
+
the entire table and indexes to be rewritten. Instead:
|
17
|
+
|
18
|
+
1. Add the column without a default value
|
19
|
+
2. Add the default value
|
20
|
+
3. Commit the transaction
|
21
|
+
4. Backfill the column",
|
22
|
+
|
23
|
+
add_column_json:
|
24
|
+
"There's no equality operator for the json column type.
|
25
|
+
Replace all calls to uniq with a custom scope.
|
26
|
+
|
27
|
+
scope :uniq_on_id, -> { select(\"DISTINCT ON (your_table.id) your_table.*\") }
|
28
|
+
|
29
|
+
Once it's deployed, wrap this step in a safety_assured { ... } block.",
|
30
|
+
|
31
|
+
change_column:
|
32
|
+
"Changing the type of an existing column requires
|
33
|
+
the entire table and indexes to be rewritten.
|
34
|
+
|
35
|
+
If you really have to:
|
36
|
+
|
37
|
+
1. Create a new column
|
38
|
+
2. Write to both columns
|
39
|
+
3. Backfill data from the old column to the new column
|
40
|
+
4. Move reads from the old column to the new column
|
41
|
+
5. Stop writing to the old column
|
42
|
+
6. Drop the old column",
|
43
|
+
|
44
|
+
remove_column:
|
45
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
46
|
+
"ActiveRecord caches attributes which causes problems
|
47
|
+
when removing columns. Be sure to ignore the column:
|
48
|
+
|
49
|
+
class User < ApplicationRecord
|
50
|
+
self.ignored_columns = %w(some_column)
|
51
|
+
end
|
52
|
+
|
53
|
+
Once that's deployed, wrap this step in a safety_assured { ... } block.
|
54
|
+
|
55
|
+
More info: https://github.com/ankane/strong_migrations#removing-a-column"
|
56
|
+
else
|
57
|
+
"ActiveRecord caches attributes which causes problems
|
58
|
+
when removing columns. Be sure to ignore the column:
|
59
|
+
|
60
|
+
class User < ActiveRecord::Base
|
61
|
+
def self.columns
|
62
|
+
super.reject { |c| c.name == \"some_column\" }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Once that's deployed, wrap this step in a safety_assured { ... } block.
|
67
|
+
|
68
|
+
More info: https://github.com/ankane/strong_migrations#removing-a-column"
|
69
|
+
end,
|
70
|
+
|
71
|
+
rename_column:
|
72
|
+
"If you really have to:
|
73
|
+
|
74
|
+
1. Create a new column
|
75
|
+
2. Write to both columns
|
76
|
+
3. Backfill data from the old column to new column
|
77
|
+
4. Move reads from the old column to the new column
|
78
|
+
5. Stop writing to the old column
|
79
|
+
6. Drop the old column",
|
80
|
+
|
81
|
+
rename_table:
|
82
|
+
"If you really have to:
|
83
|
+
|
84
|
+
1. Create a new table
|
85
|
+
2. Write to both tables
|
86
|
+
3. Backfill data from the old table to new table
|
87
|
+
4. Move reads from the old table to the new table
|
88
|
+
5. Stop writing to the old table
|
89
|
+
6. Drop the old table",
|
90
|
+
|
91
|
+
add_reference:
|
92
|
+
"Adding a non-concurrent index locks the table. Instead, use:
|
93
|
+
|
94
|
+
def change
|
95
|
+
add_reference :users, :reference, index: false
|
96
|
+
commit_db_transaction
|
97
|
+
add_index :users, :reference_id, algorithm: :concurrently
|
98
|
+
end",
|
99
|
+
|
100
|
+
add_index:
|
101
|
+
"Adding a non-concurrent index locks the table. Instead, use:
|
102
|
+
|
103
|
+
def change
|
104
|
+
commit_db_transaction
|
105
|
+
add_index :users, :some_column, algorithm: :concurrently
|
106
|
+
end",
|
107
|
+
|
108
|
+
add_index_columns:
|
109
|
+
"Adding an index with more than three columns only helps on extremely large tables.
|
110
|
+
|
111
|
+
If you're sure this is what you want, wrap it in a safety_assured { ... } block.",
|
112
|
+
|
113
|
+
change_table:
|
114
|
+
"The strong_migrations gem does not support inspecting what happens inside a
|
115
|
+
change_table block, so cannot help you here. Please make really sure that what
|
116
|
+
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
|
117
|
+
|
118
|
+
create_table:
|
119
|
+
"The force option will destroy existing tables.
|
120
|
+
If this is intended, drop the existing table first.
|
121
|
+
Otherwise, remove the option.",
|
122
|
+
|
123
|
+
execute:
|
124
|
+
"The strong_migrations gem does not support inspecting what happens inside an
|
125
|
+
execute call, so cannot help you here. Please make really sure that what
|
126
|
+
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block."
|
127
|
+
}
|
13
128
|
end
|
14
129
|
|
15
130
|
ActiveRecord::Migration.send(:prepend, StrongMigrations::Migration)
|
@@ -80,97 +80,6 @@ module StrongMigrations
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def raise_error(message_key)
|
83
|
-
message =
|
84
|
-
case message_key
|
85
|
-
when :add_column_default
|
86
|
-
"Adding a column with a non-null default requires
|
87
|
-
the entire table and indexes to be rewritten. Instead:
|
88
|
-
|
89
|
-
1. Add the column without a default value
|
90
|
-
2. Commit the transaction
|
91
|
-
3. Backfill the column
|
92
|
-
4. Add the default value"
|
93
|
-
when :add_column_json
|
94
|
-
"There's no equality operator for the json column type.
|
95
|
-
Replace all calls to uniq with a custom scope.
|
96
|
-
|
97
|
-
scope :uniq_on_id, -> { select(\"DISTINCT ON (your_table.id) your_table.*\") }
|
98
|
-
|
99
|
-
Once it's deployed, wrap this step in a safety_assured { ... } block."
|
100
|
-
when :change_column
|
101
|
-
"Changing the type of an existing column requires
|
102
|
-
the entire table and indexes to be rewritten.
|
103
|
-
|
104
|
-
If you really have to:
|
105
|
-
|
106
|
-
1. Create a new column
|
107
|
-
2. Write to both columns
|
108
|
-
3. Backfill data from the old column to the new column
|
109
|
-
4. Move reads from the old column to the new column
|
110
|
-
5. Stop writing to the old column
|
111
|
-
6. Drop the old column"
|
112
|
-
when :remove_column
|
113
|
-
"ActiveRecord caches attributes which causes problems
|
114
|
-
when removing columns. Be sure to ignored the column:
|
115
|
-
|
116
|
-
class User
|
117
|
-
def self.columns
|
118
|
-
super.reject { |c| c.name == \"some_column\" }
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
Once it's deployed, wrap this step in a safety_assured { ... } block."
|
123
|
-
when :rename_column
|
124
|
-
"If you really have to:
|
125
|
-
|
126
|
-
1. Create a new column
|
127
|
-
2. Write to both columns
|
128
|
-
3. Backfill data from the old column to new column
|
129
|
-
4. Move reads from the old column to the new column
|
130
|
-
5. Stop writing to the old column
|
131
|
-
6. Drop the old column"
|
132
|
-
when :rename_table
|
133
|
-
"If you really have to:
|
134
|
-
|
135
|
-
1. Create a new table
|
136
|
-
2. Write to both tables
|
137
|
-
3. Backfill data from the old table to new table
|
138
|
-
4. Move reads from the old table to the new table
|
139
|
-
5. Stop writing to the old table
|
140
|
-
6. Drop the old table"
|
141
|
-
when :add_reference
|
142
|
-
"Adding a non-concurrent index locks the table. Instead, use:
|
143
|
-
|
144
|
-
def change
|
145
|
-
add_reference :users, :reference, index: false
|
146
|
-
commit_db_transaction
|
147
|
-
add_index :users, :reference_id, algorithm: :concurrently
|
148
|
-
end"
|
149
|
-
when :add_index
|
150
|
-
"Adding a non-concurrent index locks the table. Instead, use:
|
151
|
-
|
152
|
-
def change
|
153
|
-
commit_db_transaction
|
154
|
-
add_index :users, :some_column, algorithm: :concurrently
|
155
|
-
end"
|
156
|
-
when :add_index_columns
|
157
|
-
"Adding an index with more than three columns only helps on extremely large tables.
|
158
|
-
|
159
|
-
If you're sure this is what you want, wrap it in a safety_assured { ... } block."
|
160
|
-
when :change_table
|
161
|
-
"The strong_migrations gem does not support inspecting what happens inside a
|
162
|
-
change_table block, so cannot help you here. Please make really sure that what
|
163
|
-
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block."
|
164
|
-
when :create_table
|
165
|
-
"The force option will destroy existing tables.
|
166
|
-
If this is intended, drop the existing table first.
|
167
|
-
Otherwise, remove the option."
|
168
|
-
when :execute
|
169
|
-
"The strong_migrations gem does not support inspecting what happens inside an
|
170
|
-
execute call, so cannot help you here. Please make really sure that what
|
171
|
-
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block."
|
172
|
-
end
|
173
|
-
|
174
83
|
wait_message = '
|
175
84
|
__ __ _____ _______ _
|
176
85
|
\ \ / /\ |_ _|__ __| |
|
@@ -180,7 +89,7 @@ you're doing is safe before proceeding, then wrap it in a safety_assured { ... }
|
|
180
89
|
\/ \/_/ \_\_____| |_| (_)
|
181
90
|
|
182
91
|
'
|
183
|
-
|
92
|
+
message = StrongMigrations.error_messages[message_key] || "Missing message"
|
184
93
|
raise StrongMigrations::UnsafeMigration, "#{wait_message}#{message}\n"
|
185
94
|
end
|
186
95
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strong_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Remeika
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: exe
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2018-01-07 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -123,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
123
|
version: '0'
|
124
124
|
requirements: []
|
125
125
|
rubyforge_project:
|
126
|
-
rubygems_version: 2.6.
|
126
|
+
rubygems_version: 2.6.13
|
127
127
|
signing_key:
|
128
128
|
specification_version: 4
|
129
129
|
summary: Catch unsafe migrations at dev time
|