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 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