strong_migrations 0.2.0 → 0.2.1

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: 5b381f6d77227aa434bfae57e8bd63b5ee74181a
4
- data.tar.gz: f1049edf7a7b01f3c075c0401003de048b7bde90
3
+ metadata.gz: 0dd3cbacfcf395cdb4ae90cb70d0da70d6411c18
4
+ data.tar.gz: 4e9d3a96ee6e57347b448b7f8bd3c7a4a412b0d5
5
5
  SHA512:
6
- metadata.gz: 28d568d129cb527e9e5465f8c583ef7cab4972da9e9142a944a899753b22f8e90346f3c1a9d63c1c3ea32b939b058716aa6734030ebca352a1a8cc507e3bd565
7
- data.tar.gz: 7c8bc5d03567a87fab67ec089c9a0ca71d2704079b5a2960530e7b97d3f76e78fd772a949d5e36b19582eea49024cdd2a7e7e6a8062344ae673a70a52be41d96
6
+ metadata.gz: fb8c4bc25cdea84f541199bf56cfdf27d64d8667e3c740729c06a1fdd3de2116c6b8d0ed794b8dc8a67486a7b84262c78f1a4dd428bf5a67518d00f2e9efadae
7
+ data.tar.gz: 5e3eef7979383a2dd249ddbfd15c0e7fca2caaad288b69acb10ed1f4cec3ad6d8155bb004dc1daba35b36dcc2a08abc99304dad0a7112327635b802b7c927775
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.2.1
2
+
3
+ - Recommend `disable_ddl_transaction!` over `commit_db_transaction`
4
+ - Suggest `jsonb` over `json` in Postgres 9.4+
5
+ - Changing `varchar` to `text` is safe in Postgres 9.1+
6
+ - Do not check number of columns for unique indexes
7
+
1
8
  ## 0.2.0
2
9
 
3
10
  - Added customizable error messages
data/README.md CHANGED
@@ -14,55 +14,59 @@ Add this line to your application’s Gemfile:
14
14
  gem 'strong_migrations'
15
15
  ```
16
16
 
17
+ ## How It Works
18
+
19
+ Strong Migrations detects potentially dangerous operations in migrations, prevents them from running, and gives instructions on safer ways to do what you want.
20
+
21
+ ```
22
+ __ __ _____ _______ _
23
+ \ \ / /\ |_ _|__ __| |
24
+ \ \ /\ / / \ | | | | | |
25
+ \ \/ \/ / /\ \ | | | | | |
26
+ \ /\ / ____ \ _| |_ | | |_|
27
+ \/ \/_/ \_\_____| |_| (_)
28
+
29
+ ActiveRecord caches attributes which causes problems
30
+ when removing columns. Be sure to ignore the column:
31
+
32
+ class User < ApplicationRecord
33
+ self.ignored_columns = %w(some_column)
34
+ end
35
+
36
+ Once that's deployed, wrap this step in a safety_assured { ... } block.
37
+
38
+ More info: https://github.com/ankane/strong_migrations#removing-a-column
39
+ ```
40
+
17
41
  ## Dangerous Operations
18
42
 
19
43
  The following operations can cause downtime or errors:
20
44
 
21
45
  - adding a column with a non-null default value to an existing table
46
+ - removing a column
22
47
  - changing the type of a column
23
- - renaming a table
24
48
  - renaming a column
25
- - removing a column
49
+ - renaming a table
26
50
  - adding an index non-concurrently (Postgres only)
27
51
  - adding a `json` column to an existing table (Postgres only)
28
52
 
29
- For more info, check out:
30
-
31
- - [Rails Migrations with No Downtime](http://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
32
- - [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/) (if it’s relevant)
33
-
34
53
  Also checks for best practices:
35
54
 
36
- - keeping indexes to three columns or less
55
+ - keeping non-unique indexes to three columns or less
37
56
 
38
57
  ## The Zero Downtime Way
39
58
 
40
59
  ### Adding a column with a default value
41
60
 
42
- 1. Add the column without a 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
61
+ Adding a column with a non-null default causes the entire table to be rewritten.
62
+
63
+ Instead, add the column without a default value, then change the default.
46
64
 
47
65
  ```ruby
48
- class AddSomeColumnToUsers < ActiveRecord::Migration
66
+ class AddSomeColumnToUsers < ActiveRecord::Migration[5.1]
49
67
  def up
50
- # 1
51
68
  add_column :users, :some_column, :text
52
-
53
- # 2
54
69
  change_column_default :users, :some_column, "default_value"
55
-
56
- # 3
57
- commit_db_transaction
58
-
59
- # 4.a (Rails 5+)
60
- User.in_batches.update_all some_column: "default_value"
61
-
62
- # 4.b (Rails < 5)
63
- User.find_in_batches do |users|
64
- User.where(id: users.map(&:id)).update_all some_column: "default_value"
65
- end
66
70
  end
67
71
 
68
72
  def down
@@ -71,27 +75,25 @@ class AddSomeColumnToUsers < ActiveRecord::Migration
71
75
  end
72
76
  ```
73
77
 
74
- ### Renaming or changing the type of a column
78
+ ### Backfilling data
75
79
 
76
- If you really have to:
80
+ To backfill data, use the Rails console or a separate migration with `disable_ddl_transaction!`. Avoid backfilling in a transaction, especially one that alters a table. See [this great article](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/) on why.
77
81
 
78
- 1. Create a new column
79
- 2. Write to both columns
80
- 3. Backfill data from the old column to the new column
81
- 4. Move reads from the old column to the new column
82
- 5. Stop writing to the old column
83
- 6. Drop the old column
84
-
85
- ### Renaming a table
82
+ ```ruby
83
+ class BackfillSomeColumn < ActiveRecord::Migration[5.1]
84
+ disable_ddl_transaction!
86
85
 
87
- If you really have to:
86
+ def change
87
+ # Rails 5+
88
+ User.in_batches.update_all some_column: "default_value"
88
89
 
89
- 1. Create a new table
90
- 2. Write to both tables
91
- 3. Backfill data from the old table to new table
92
- 4. Move reads from the old table to the new table
93
- 5. Stop writing to the old table
94
- 6. Drop the old table
90
+ # Rails < 5
91
+ User.find_in_batches do |users|
92
+ User.where(id: users.map(&:id)).update_all some_column: "default_value"
93
+ end
94
+ end
95
+ end
96
+ ```
95
97
 
96
98
  ### Removing a column
97
99
 
@@ -117,7 +119,7 @@ ActiveRecord caches database columns at runtime, so if you drop a column, it can
117
119
  3. Write a migration to remove the column (wrap in `safety_assured` block)
118
120
 
119
121
  ```ruby
120
- class RemoveSomeColumnFromUsers < ActiveRecord::Migration
122
+ class RemoveSomeColumnFromUsers < ActiveRecord::Migration[5.1]
121
123
  def change
122
124
  safety_assured { remove_column :users, :some_column }
123
125
  end
@@ -126,25 +128,70 @@ ActiveRecord caches database columns at runtime, so if you drop a column, it can
126
128
 
127
129
  4. Deploy and run migration
128
130
 
131
+ ### Renaming or changing the type of a column
132
+
133
+ If you really have to:
134
+
135
+ 1. Create a new column
136
+ 2. Write to both columns
137
+ 3. Backfill data from the old column to the new column
138
+ 4. Move reads from the old column to the new column
139
+ 5. Stop writing to the old column
140
+ 6. Drop the old column
141
+
142
+ One exception is changing a `varchar` column to `text`, which is safe in Postgres 9.1+.
143
+
144
+ ### Renaming a table
145
+
146
+ If you really have to:
147
+
148
+ 1. Create a new table
149
+ 2. Write to both tables
150
+ 3. Backfill data from the old table to new table
151
+ 4. Move reads from the old table to the new table
152
+ 5. Stop writing to the old table
153
+ 6. Drop the old table
154
+
129
155
  ### Adding an index (Postgres)
130
156
 
131
157
  Add indexes concurrently.
132
158
 
133
159
  ```ruby
134
- class AddSomeIndexToUsers < ActiveRecord::Migration
160
+ class AddSomeIndexToUsers < ActiveRecord::Migration[5.1]
161
+ disable_ddl_transaction!
162
+
135
163
  def change
136
- commit_db_transaction
137
164
  add_index :users, :some_index, algorithm: :concurrently
138
165
  end
139
166
  end
140
167
  ```
141
168
 
169
+ If you forget `disable_ddl_transaction!`, the migration will fail.
170
+
171
+ Also, note that indexes on new tables (those created in the same migration) don’t require this.
172
+
142
173
  ### Adding a json column (Postgres)
143
174
 
144
- There’s no equality operator for the `json` column type, which causes issues for `SELECT DISTINCT` queries. Replace all calls to `uniq` with a custom scope.
175
+ There’s no equality operator for the `json` column type, which causes issues for `SELECT DISTINCT` queries.
176
+
177
+ If you’re on Postgres 9.4+, use `jsonb` instead.
178
+
179
+ If you must use `json`, replace all calls to `uniq` with a custom scope.
145
180
 
146
181
  ```ruby
147
- scope :uniq_on_id, -> { select("DISTINCT ON (your_table.id) your_table.*") }
182
+ class User < ApplicationRecord
183
+ scope :uniq_on_id, -> { select("DISTINCT ON (users.id) users.*") }
184
+ end
185
+ ```
186
+
187
+ Then add the column:
188
+
189
+ ```ruby
190
+ class AddJsonColumnToUsers < ActiveRecord::Migration[5.1]
191
+ def change
192
+ safety_assured { add_column :users, :some_column, :json }
193
+ end
194
+ end
148
195
  ```
149
196
 
150
197
  ## Assuring Safety
@@ -152,7 +199,7 @@ scope :uniq_on_id, -> { select("DISTINCT ON (your_table.id) your_table.*") }
152
199
  To mark a step in the migration as safe, despite using method that might otherwise be dangerous, wrap it in a `safety_assured` block.
153
200
 
154
201
  ```ruby
155
- class MySafeMigration < ActiveRecord::Migration
202
+ class MySafeMigration < ActiveRecord::Migration[5.1]
156
203
  def change
157
204
  safety_assured { remove_column :users, :some_column }
158
205
  end
@@ -194,15 +241,15 @@ Columns can flip order in `db/schema.rb` when you have multiple developers. One
194
241
  task "db:schema:dump": "strong_migrations:alphabetize_columns"
195
242
  ```
196
243
 
197
- ## Custom Error Messages
244
+ ## Custom Messages
198
245
 
199
- To customize specific error messages, create an initializer with:
246
+ To customize specific messages, create an initializer with:
200
247
 
201
248
  ```ruby
202
249
  StrongMigrations.error_messages[:add_column_default] = "Your custom instructions"
203
250
  ```
204
251
 
205
- Check the source code for the list of keys.
252
+ Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
206
253
 
207
254
  ## Analyze Tables (Postgres)
208
255
 
@@ -222,6 +269,11 @@ ALTER ROLE myuser SET lock_timeout = '10s';
222
269
 
223
270
  There’s also [a gem](https://github.com/gocardless/activerecord-safer_migrations) you can use for this.
224
271
 
272
+ ## Additional Reading
273
+
274
+ - [Rails Migrations with No Downtime](http://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
275
+ - [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/)
276
+
225
277
  ## Credits
226
278
 
227
279
  Thanks to Bob Remeika and David Waller for the [original code](https://github.com/foobarfighter/safe-migrations).
@@ -12,15 +12,27 @@ module StrongMigrations
12
12
  self.start_after = 0
13
13
  self.error_messages = {
14
14
  add_column_default:
15
- "Adding a column with a non-null default requires
16
- the entire table and indexes to be rewritten. Instead:
15
+ "Adding a column with a non-null default causes
16
+ the entire table to be rewritten.
17
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",
18
+ Instead, add the column without a default value,
19
+ then change the default.
20
+
21
+ def up
22
+ add_column :users, :some_column, :text
23
+ change_column_default :users, :some_column, \"default_value\"
24
+ end
25
+
26
+ def down
27
+ remove_column :users, :some_column
28
+ end
29
+
30
+ More info: https://github.com/ankane/strong_migrations#adding-a-column-with-a-default-value",
22
31
 
23
32
  add_column_json:
33
+ "Use jsonb instead.",
34
+
35
+ add_column_json_legacy:
24
36
  "There's no equality operator for the json column type.
25
37
  Replace all calls to uniq with a custom scope.
26
38
 
@@ -91,17 +103,19 @@ More info: https://github.com/ankane/strong_migrations#removing-a-column"
91
103
  add_reference:
92
104
  "Adding a non-concurrent index locks the table. Instead, use:
93
105
 
106
+ disable_ddl_transaction!
107
+
94
108
  def change
95
109
  add_reference :users, :reference, index: false
96
- commit_db_transaction
97
110
  add_index :users, :reference_id, algorithm: :concurrently
98
111
  end",
99
112
 
100
113
  add_index:
101
114
  "Adding a non-concurrent index locks the table. Instead, use:
102
115
 
116
+ disable_ddl_transaction!
117
+
103
118
  def change
104
- commit_db_transaction
105
119
  add_index :users, :some_column, algorithm: :concurrently
106
120
  end",
107
121
 
@@ -28,10 +28,10 @@ module StrongMigrations
28
28
  raise_error :rename_column
29
29
  when :add_index
30
30
  columns = args[1]
31
- if columns.is_a?(Array) && columns.size > 3
31
+ options = args[2] || {}
32
+ if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
32
33
  raise_error :add_index_columns
33
34
  end
34
- options = args[2] || {}
35
35
  if postgresql? && options[:algorithm] != :concurrently && !@new_tables.to_a.include?(args[0].to_s)
36
36
  raise_error :add_index
37
37
  end
@@ -39,9 +39,21 @@ module StrongMigrations
39
39
  type = args[2]
40
40
  options = args[3] || {}
41
41
  raise_error :add_column_default unless options[:default].nil?
42
- raise_error :add_column_json if type.to_s == "json"
42
+ if type.to_s == "json" && postgresql?
43
+ if postgresql_version >= 90400
44
+ raise_error :add_column_json
45
+ else
46
+ raise_error :add_column_json_legacy
47
+ end
48
+ end
43
49
  when :change_column
44
- raise_error :change_column
50
+ safe = false
51
+ # assume Postgres 9.1+ since previous versions are EOL
52
+ if postgresql? && args[2].to_s == "text"
53
+ column = connection.columns(args[0]).find { |c| c.name.to_s == args[1].to_s }
54
+ safe = column && column.type == :string
55
+ end
56
+ raise_error :change_column unless safe
45
57
  when :create_table
46
58
  options = args[1] || {}
47
59
  raise_error :create_table if options[:force]
@@ -75,6 +87,10 @@ module StrongMigrations
75
87
  %w(PostgreSQL PostGIS).include?(connection.adapter_name)
76
88
  end
77
89
 
90
+ def postgresql_version
91
+ @postgresql_version ||= connection.execute("SHOW server_version_num").first["server_version_num"].to_i
92
+ end
93
+
78
94
  def version_safe?
79
95
  version && version <= StrongMigrations.start_after
80
96
  end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -22,5 +22,5 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency "bundler"
23
23
  spec.add_development_dependency "rake"
24
24
  spec.add_development_dependency "minitest"
25
- spec.add_development_dependency "pg"
25
+ spec.add_development_dependency "pg", "< 1"
26
26
  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.2.0
4
+ version: 0.2.1
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: 2018-01-07 00:00:00.000000000 Z
13
+ date: 2018-02-08 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -72,16 +72,16 @@ dependencies:
72
72
  name: pg
73
73
  requirement: !ruby/object:Gem::Requirement
74
74
  requirements:
75
- - - ">="
75
+ - - "<"
76
76
  - !ruby/object:Gem::Version
77
- version: '0'
77
+ version: '1'
78
78
  type: :development
79
79
  prerelease: false
80
80
  version_requirements: !ruby/object:Gem::Requirement
81
81
  requirements:
82
- - - ">="
82
+ - - "<"
83
83
  - !ruby/object:Gem::Version
84
- version: '0'
84
+ version: '1'
85
85
  description:
86
86
  email:
87
87
  - bob.remeika@gmail.com