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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 85bce3e28cb77bab6bea2ff5fe300c7e5b7b7b26
4
- data.tar.gz: aa648b6c6cb9744cfd9bad3fa0af93544e62d056
3
+ metadata.gz: 5b381f6d77227aa434bfae57e8bd63b5ee74181a
4
+ data.tar.gz: f1049edf7a7b01f3c075c0401003de048b7bde90
5
5
  SHA512:
6
- metadata.gz: 0327c5add8b8d83026e1d2044740f115918c3ec588db1c09bf97130354bd812c59ac78ab0f9d2f13cff12c84593e5406b4a05d1fa1a426ed478869adcc7c94ba
7
- data.tar.gz: 335feda7ce4b8fd0a55a52ea6272ac413e6bbffe1b308f2db9fc758ff0a0568a0a6a4b0e238dce5490979caee04568a4ab51c81b5a3c26efcc82d8db0d022b2a
6
+ metadata.gz: 28d568d129cb527e9e5465f8c583ef7cab4972da9e9142a944a899753b22f8e90346f3c1a9d63c1c3ea32b939b058716aa6734030ebca352a1a8cc507e3bd565
7
+ data.tar.gz: 7c8bc5d03567a87fab67ec089c9a0ca71d2704079b5a2960530e7b97d3f76e78fd772a949d5e36b19582eea49024cdd2a7e7e6a8062344ae673a70a52be41d96
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.2.0
2
+
3
+ - Added customizable error messages
4
+ - Updated instructions for adding a column with a default value
5
+
1
6
  ## 0.1.9
2
7
 
3
8
  - Added `start_after` option
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. Commit the transaction
43
- 3. Backfill the column
44
- 4. Add the default value
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
- # 3.a (Rails 5+)
59
+ # 4.a (Rails 5+)
56
60
  User.in_batches.update_all some_column: "default_value"
57
61
 
58
- # 3.b (Rails < 5)
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
- Tell ActiveRecord to ignore the column from its cache.
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
- ```ruby
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
- # For Rails < 5
106
- class User < ActiveRecord::Base
107
- def self.columns
108
- super.reject { |c| c.name == "some_column" }
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
- Once it’s deployed, create a migration to remove the column.
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).
@@ -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
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.1.9"
2
+ VERSION = "0.2.0"
3
3
  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.1.9
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: 2017-06-14 00:00:00.000000000 Z
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.11
126
+ rubygems_version: 2.6.13
127
127
  signing_key:
128
128
  specification_version: 4
129
129
  summary: Catch unsafe migrations at dev time