strong_migrations 0.1.9 → 0.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.
- 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
|
-
[](https://travis-ci.org/ankane/strong_migrations)
|
7
|
+
[](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
|