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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +105 -53
- data/lib/strong_migrations.rb +22 -8
- data/lib/strong_migrations/migration.rb +20 -4
- data/lib/strong_migrations/version.rb +1 -1
- data/strong_migrations.gemspec +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0dd3cbacfcf395cdb4ae90cb70d0da70d6411c18
|
4
|
+
data.tar.gz: 4e9d3a96ee6e57347b448b7f8bd3c7a4a412b0d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
###
|
78
|
+
### Backfilling data
|
75
79
|
|
76
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
86
|
+
def change
|
87
|
+
# Rails 5+
|
88
|
+
User.in_batches.update_all some_column: "default_value"
|
88
89
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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.
|
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
|
-
|
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
|
244
|
+
## Custom Messages
|
198
245
|
|
199
|
-
To customize specific
|
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).
|
data/lib/strong_migrations.rb
CHANGED
@@ -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
|
16
|
-
the entire table
|
15
|
+
"Adding a column with a non-null default causes
|
16
|
+
the entire table to be rewritten.
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/strong_migrations.gemspec
CHANGED
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.
|
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-
|
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: '
|
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: '
|
84
|
+
version: '1'
|
85
85
|
description:
|
86
86
|
email:
|
87
87
|
- bob.remeika@gmail.com
|