strong_migrations 0.2.0 → 0.2.1

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