strong_migrations 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +9 -7
- data/README.md +54 -25
- data/lib/strong_migrations.rb +64 -51
- data/lib/strong_migrations/migration.rb +125 -31
- data/lib/strong_migrations/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08eb5f509b5fb98501eb6f83840c414c331c989b7d92bd29825cbaa0e2312177'
|
4
|
+
data.tar.gz: 0fc4b64d44609ff2f6bbbf48d53730e35551654d3a1aaf629b7305ab8ac92caf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc5cbfa0dbcdc47b8f5e42c18c64d77cabcc01deac0e92077d9d034bff6a47145354da066c70d2eb2906c60c895f5639de40ee9a1c00e015f80d54242f8bcb4c
|
7
|
+
data.tar.gz: 5dfcdd45c56da317a83509c6f79c3d81b8c35e3f6e8aaf0af32c8144e134b919da481b2f501a87dbb581f4c61db96bccc4103cf285210f7777f0258020e1c6f3
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## 0.3.0
|
2
|
+
|
3
|
+
- Added support for custom checks
|
4
|
+
- Adding a column with a non-null default value is safe in Postgres 11+
|
5
|
+
- Added checks for `add_belongs_to`, `remove_belongs_to`, `remove_columns`, and `remove_reference`
|
6
|
+
- Customized messages
|
7
|
+
|
1
8
|
## 0.2.3
|
2
9
|
|
3
10
|
- Added check for `change_column_null`
|
data/CONTRIBUTING.md
CHANGED
@@ -2,17 +2,15 @@
|
|
2
2
|
|
3
3
|
First, thanks for wanting to contribute. You’re awesome! :heart:
|
4
4
|
|
5
|
-
##
|
5
|
+
## Help
|
6
6
|
|
7
|
-
|
7
|
+
We’re not able to provide support through GitHub Issues. If you’re looking for help with your code, try posting on [Stack Overflow](https://stackoverflow.com/).
|
8
8
|
|
9
|
-
|
9
|
+
All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
|
10
10
|
|
11
|
-
|
11
|
+
## Bugs
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
Think you’ve discovered an issue?
|
13
|
+
Think you’ve discovered a bug?
|
16
14
|
|
17
15
|
1. Search existing issues to see if it’s been reported.
|
18
16
|
2. Try the `master` branch to make sure it hasn’t been fixed.
|
@@ -26,6 +24,10 @@ If the above steps don’t help, create an issue. Include:
|
|
26
24
|
- Detailed steps to reproduce
|
27
25
|
- Complete backtraces for exceptions
|
28
26
|
|
27
|
+
## New Features
|
28
|
+
|
29
|
+
If you’d like to discuss a new feature, create an issue and start the title with `[Idea]`.
|
30
|
+
|
29
31
|
## Pull Requests
|
30
32
|
|
31
33
|
Fork the project and create a pull request. A few tips:
|
data/README.md
CHANGED
@@ -16,26 +16,25 @@ gem 'strong_migrations'
|
|
16
16
|
|
17
17
|
## How It Works
|
18
18
|
|
19
|
-
Strong Migrations detects potentially dangerous operations in migrations, prevents them from running by default, and provides instructions on safer ways to do what you want.
|
19
|
+
Strong Migrations detects potentially dangerous operations in migrations, prevents them from running by default, and provides instructions on safer ways to do what you want. Here’s an example:
|
20
20
|
|
21
21
|
```
|
22
|
-
|
23
|
-
\ \ / /\ |_ _|__ __| |
|
24
|
-
\ \ /\ / / \ | | | | | |
|
25
|
-
\ \/ \/ / /\ \ | | | | | |
|
26
|
-
\ /\ / ____ \ _| |_ | | |_|
|
27
|
-
\/ \/_/ \_\_____| |_| (_) #strong_migrations
|
22
|
+
=== Dangerous operation detected #strong_migrations ===
|
28
23
|
|
29
24
|
ActiveRecord caches attributes which causes problems
|
30
25
|
when removing columns. Be sure to ignore the column:
|
31
26
|
|
32
27
|
class User < ApplicationRecord
|
33
|
-
self.ignored_columns =
|
28
|
+
self.ignored_columns = ["some_column"]
|
34
29
|
end
|
35
30
|
|
36
|
-
|
31
|
+
Deploy the code, then wrap this step in a safety_assured { ... } block.
|
37
32
|
|
38
|
-
|
33
|
+
class RemoveColumn < ActiveRecord::Migration[5.2]
|
34
|
+
def change
|
35
|
+
safety_assured { remove_column :users, :some_column }
|
36
|
+
end
|
37
|
+
end
|
39
38
|
```
|
40
39
|
|
41
40
|
## Dangerous Operations
|
@@ -48,6 +47,7 @@ The following operations can cause downtime or errors:
|
|
48
47
|
- setting a `NOT NULL` constraint with a default value
|
49
48
|
- renaming a column
|
50
49
|
- renaming a table
|
50
|
+
- creating a table with the `force` option
|
51
51
|
- adding an index non-concurrently (Postgres only)
|
52
52
|
- adding a `json` column to an existing table (Postgres only)
|
53
53
|
|
@@ -64,7 +64,7 @@ Adding a column with a non-null default causes the entire table to be rewritten.
|
|
64
64
|
Instead, add the column without a default value, then change the default.
|
65
65
|
|
66
66
|
```ruby
|
67
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[5.
|
67
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[5.2]
|
68
68
|
def up
|
69
69
|
add_column :users, :some_column, :text
|
70
70
|
change_column_default :users, :some_column, "default_value"
|
@@ -78,12 +78,14 @@ end
|
|
78
78
|
|
79
79
|
Don’t backfill existing rows in this migration, as it can cause downtime. See the next section for how to do it safely.
|
80
80
|
|
81
|
+
> With Postgres, this operation is safe as of Postgres 11
|
82
|
+
|
81
83
|
### Backfilling data
|
82
84
|
|
83
85
|
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.
|
84
86
|
|
85
87
|
```ruby
|
86
|
-
class BackfillSomeColumn < ActiveRecord::Migration[5.
|
88
|
+
class BackfillSomeColumn < ActiveRecord::Migration[5.2]
|
87
89
|
disable_ddl_transaction!
|
88
90
|
|
89
91
|
def change
|
@@ -91,8 +93,8 @@ class BackfillSomeColumn < ActiveRecord::Migration[5.1]
|
|
91
93
|
User.in_batches.update_all some_column: "default_value"
|
92
94
|
|
93
95
|
# Rails < 5
|
94
|
-
User.find_in_batches do |
|
95
|
-
User.where(id:
|
96
|
+
User.find_in_batches do |records|
|
97
|
+
User.where(id: records.map(&:id)).update_all some_column: "default_value"
|
96
98
|
end
|
97
99
|
end
|
98
100
|
end
|
@@ -107,7 +109,7 @@ ActiveRecord caches database columns at runtime, so if you drop a column, it can
|
|
107
109
|
```ruby
|
108
110
|
# For Rails 5+
|
109
111
|
class User < ApplicationRecord
|
110
|
-
self.ignored_columns =
|
112
|
+
self.ignored_columns = ["some_column"]
|
111
113
|
end
|
112
114
|
|
113
115
|
# For Rails < 5
|
@@ -122,7 +124,7 @@ ActiveRecord caches database columns at runtime, so if you drop a column, it can
|
|
122
124
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
123
125
|
|
124
126
|
```ruby
|
125
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[5.
|
127
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[5.2]
|
126
128
|
def change
|
127
129
|
safety_assured { remove_column :users, :some_column }
|
128
130
|
end
|
@@ -133,7 +135,7 @@ ActiveRecord caches database columns at runtime, so if you drop a column, it can
|
|
133
135
|
|
134
136
|
### Renaming or changing the type of a column
|
135
137
|
|
136
|
-
|
138
|
+
A safer approach is to:
|
137
139
|
|
138
140
|
1. Create a new column
|
139
141
|
2. Write to both columns
|
@@ -146,7 +148,7 @@ One exception is changing a `varchar` column to `text`, which is safe in Postgre
|
|
146
148
|
|
147
149
|
### Renaming a table
|
148
150
|
|
149
|
-
|
151
|
+
A safer approach is to:
|
150
152
|
|
151
153
|
1. Create a new table
|
152
154
|
2. Write to both tables
|
@@ -160,18 +162,31 @@ If you really have to:
|
|
160
162
|
Add indexes concurrently.
|
161
163
|
|
162
164
|
```ruby
|
163
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[5.
|
165
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[5.2]
|
164
166
|
disable_ddl_transaction!
|
165
167
|
|
166
168
|
def change
|
167
|
-
add_index :users, :
|
169
|
+
add_index :users, :some_column, algorithm: :concurrently
|
168
170
|
end
|
169
171
|
end
|
170
172
|
```
|
171
173
|
|
172
|
-
If you forget `disable_ddl_transaction!`, the migration will fail. Also, note that indexes on new tables (those created in the same migration) don’t require this.
|
174
|
+
If you forget `disable_ddl_transaction!`, the migration will fail. Also, note that indexes on new tables (those created in the same migration) don’t require this. Check out [gindex](https://github.com/ankane/gindex) to quickly generate index migrations without memorizing the syntax.
|
175
|
+
|
176
|
+
Rails 5+ adds an index to references by default. To make sure this happens concurrently, use:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class AddSomeReferenceToUsers < ActiveRecord::Migration[5.2]
|
180
|
+
disable_ddl_transaction!
|
181
|
+
|
182
|
+
def change
|
183
|
+
add_reference :users, :reference, index: false
|
184
|
+
add_index :users, :reference_id, algorithm: :concurrently
|
185
|
+
end
|
186
|
+
end
|
187
|
+
```
|
173
188
|
|
174
|
-
|
189
|
+
For polymorphic references, add a compound index on type and id.
|
175
190
|
|
176
191
|
### Adding a json column (Postgres)
|
177
192
|
|
@@ -190,7 +205,7 @@ end
|
|
190
205
|
Then add the column:
|
191
206
|
|
192
207
|
```ruby
|
193
|
-
class AddJsonColumnToUsers < ActiveRecord::Migration[5.
|
208
|
+
class AddJsonColumnToUsers < ActiveRecord::Migration[5.2]
|
194
209
|
def change
|
195
210
|
safety_assured { add_column :users, :some_column, :json }
|
196
211
|
end
|
@@ -202,13 +217,27 @@ end
|
|
202
217
|
To mark a step in the migration as safe, despite using method that might otherwise be dangerous, wrap it in a `safety_assured` block.
|
203
218
|
|
204
219
|
```ruby
|
205
|
-
class MySafeMigration < ActiveRecord::Migration[5.
|
220
|
+
class MySafeMigration < ActiveRecord::Migration[5.2]
|
206
221
|
def change
|
207
222
|
safety_assured { remove_column :users, :some_column }
|
208
223
|
end
|
209
224
|
end
|
210
225
|
```
|
211
226
|
|
227
|
+
## Custom Checks
|
228
|
+
|
229
|
+
Add your own custom checks with:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
StrongMigrations.add_check do |method, args|
|
233
|
+
if method == :add_index && args[0].to_s == "users"
|
234
|
+
stop! "No more indexes on the users table"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
Use the `stop!` method to stop migrations.
|
240
|
+
|
212
241
|
## Existing Migrations
|
213
242
|
|
214
243
|
To mark migrations as safe that were created before installing this gem, create an initializer with:
|
@@ -274,7 +303,7 @@ There’s also [a gem](https://github.com/gocardless/activerecord-safer_migratio
|
|
274
303
|
|
275
304
|
## Bigint Primary Keys (Postgres & MySQL)
|
276
305
|
|
277
|
-
Rails 5.1+ uses `bigint` for primary keys to keep you from running out of ids. To get this in earlier versions of Rails, check out [
|
306
|
+
Rails 5.1+ uses `bigint` for primary keys to keep you from running out of ids. To get this in earlier versions of Rails, check out [rails-bigint-primarykey](https://github.com/Shopify/rails-bigint-primarykey).
|
278
307
|
|
279
308
|
## Additional Reading
|
280
309
|
|
data/lib/strong_migrations.rb
CHANGED
@@ -8,45 +8,55 @@ require "strong_migrations/version"
|
|
8
8
|
|
9
9
|
module StrongMigrations
|
10
10
|
class << self
|
11
|
-
attr_accessor :auto_analyze, :start_after, :error_messages
|
11
|
+
attr_accessor :auto_analyze, :start_after, :checks, :error_messages
|
12
12
|
end
|
13
13
|
self.auto_analyze = false
|
14
14
|
self.start_after = 0
|
15
|
+
self.checks = []
|
15
16
|
self.error_messages = {
|
16
17
|
add_column_default:
|
17
|
-
"Adding a column with a non-null default causes
|
18
|
-
the
|
19
|
-
|
20
|
-
Instead, add the column without a default value,
|
21
|
-
then change the default.
|
18
|
+
"Adding a column with a non-null default causes the entire table to be rewritten.
|
19
|
+
Instead, add the column without a default value, then change the default.
|
22
20
|
|
21
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
23
22
|
def up
|
24
|
-
add_column
|
25
|
-
change_column_default
|
23
|
+
add_column %{table}, %{column}, %{type}%{options}
|
24
|
+
change_column_default %{table}, %{column}, %{default}
|
26
25
|
end
|
27
26
|
|
28
27
|
def down
|
29
|
-
remove_column
|
28
|
+
remove_column %{table}, %{column}
|
30
29
|
end
|
30
|
+
end
|
31
31
|
|
32
|
-
|
32
|
+
Then backfill the existing rows in the Rails console or a separate migration with disable_ddl_transaction!.
|
33
|
+
|
34
|
+
class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
35
|
+
disable_ddl_transaction!
|
36
|
+
|
37
|
+
def change
|
38
|
+
%{code}
|
39
|
+
end
|
40
|
+
end",
|
33
41
|
|
34
42
|
add_column_json:
|
35
|
-
"
|
43
|
+
"There's no equality operator for the json column type, which
|
44
|
+
causes issues for SELECT DISTINCT queries. Use jsonb instead.",
|
36
45
|
|
37
46
|
add_column_json_legacy:
|
38
|
-
"There's no equality operator for the json column type.
|
47
|
+
"There's no equality operator for the json column type, which.
|
48
|
+
causes issues for SELECT DISTINCT queries.
|
39
49
|
Replace all calls to uniq with a custom scope.
|
40
50
|
|
41
|
-
|
51
|
+
class %{model} < %{base_model}
|
52
|
+
scope :uniq_on_id, -> { select('DISTINCT ON (%{table}.id) %{table}.*') }
|
53
|
+
end
|
42
54
|
|
43
55
|
Once it's deployed, wrap this step in a safety_assured { ... } block.",
|
44
56
|
|
45
57
|
change_column:
|
46
|
-
"Changing the type of an existing column requires
|
47
|
-
|
48
|
-
|
49
|
-
If you really have to:
|
58
|
+
"Changing the type of an existing column requires the entire
|
59
|
+
table and indexes to be rewritten. A safer approach is to:
|
50
60
|
|
51
61
|
1. Create a new column
|
52
62
|
2. Write to both columns
|
@@ -56,18 +66,22 @@ If you really have to:
|
|
56
66
|
6. Drop the old column",
|
57
67
|
|
58
68
|
remove_column: "ActiveRecord caches attributes which causes problems
|
59
|
-
when removing columns. Be sure to ignore the column:
|
69
|
+
when removing columns. Be sure to ignore the column%{column_suffix}:
|
60
70
|
|
61
|
-
class
|
62
|
-
|
71
|
+
class %{model} < %{base_model}
|
72
|
+
%{code}
|
63
73
|
end
|
64
74
|
|
65
|
-
|
75
|
+
Deploy the code, then wrap this step in a safety_assured { ... } block.
|
66
76
|
|
67
|
-
|
77
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
78
|
+
def change
|
79
|
+
safety_assured { %{command} }
|
80
|
+
end
|
81
|
+
end",
|
68
82
|
|
69
83
|
rename_column:
|
70
|
-
"
|
84
|
+
"Renaming a column is dangerous. A safer approach is to:
|
71
85
|
|
72
86
|
1. Create a new column
|
73
87
|
2. Write to both columns
|
@@ -77,7 +91,7 @@ More info: https://github.com/ankane/strong_migrations#removing-a-column",
|
|
77
91
|
6. Drop the old column",
|
78
92
|
|
79
93
|
rename_table:
|
80
|
-
"
|
94
|
+
"Renaming a table is dangerous. A safer approach is to:
|
81
95
|
|
82
96
|
1. Create a new table
|
83
97
|
2. Write to both tables
|
@@ -89,21 +103,25 @@ More info: https://github.com/ankane/strong_migrations#removing-a-column",
|
|
89
103
|
add_reference:
|
90
104
|
"Adding a non-concurrent index locks the table. Instead, use:
|
91
105
|
|
106
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
92
107
|
disable_ddl_transaction!
|
93
108
|
|
94
109
|
def change
|
95
|
-
|
96
|
-
add_index
|
97
|
-
end
|
110
|
+
%{command} %{table}, %{reference}, index: false%{options}
|
111
|
+
add_index %{table}, %{column}, algorithm: :concurrently
|
112
|
+
end
|
113
|
+
end",
|
98
114
|
|
99
115
|
add_index:
|
100
116
|
"Adding a non-concurrent index locks the table. Instead, use:
|
101
117
|
|
118
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
102
119
|
disable_ddl_transaction!
|
103
120
|
|
104
121
|
def change
|
105
|
-
add_index
|
106
|
-
end
|
122
|
+
add_index %{table}, %{column}, algorithm: :concurrently%{options}
|
123
|
+
end
|
124
|
+
end",
|
107
125
|
|
108
126
|
add_index_columns:
|
109
127
|
"Adding an index with more than three columns only helps on extremely large tables.
|
@@ -111,46 +129,41 @@ More info: https://github.com/ankane/strong_migrations#removing-a-column",
|
|
111
129
|
If you're sure this is what you want, wrap it in a safety_assured { ... } block.",
|
112
130
|
|
113
131
|
change_table:
|
114
|
-
"
|
132
|
+
"Strong Migrations does not support inspecting what happens inside a
|
115
133
|
change_table block, so cannot help you here. Please make really sure that what
|
116
134
|
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
|
117
135
|
|
118
136
|
create_table:
|
119
137
|
"The force option will destroy existing tables.
|
120
138
|
If this is intended, drop the existing table first.
|
121
|
-
Otherwise, remove the option.",
|
139
|
+
Otherwise, remove the force option.",
|
122
140
|
|
123
141
|
execute:
|
124
|
-
"
|
142
|
+
"Strong Migrations does not support inspecting what happens inside an
|
125
143
|
execute call, so cannot help you here. Please make really sure that what
|
126
144
|
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
|
127
145
|
|
128
146
|
change_column_null:
|
129
|
-
"
|
130
|
-
|
131
|
-
|
147
|
+
"Passing a default value to change_column_null runs a single UPDATE query,
|
148
|
+
which can cause downtime. Instead, backfill the existing rows in the
|
149
|
+
Rails console or a separate migration with disable_ddl_transaction!.
|
132
150
|
|
133
|
-
|
134
|
-
|
135
|
-
end
|
136
|
-
|
137
|
-
ActiveSupport.on_load(:active_record) do
|
138
|
-
ActiveRecord::Migration.prepend(StrongMigrations::Migration)
|
151
|
+
class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
152
|
+
disable_ddl_transaction!
|
139
153
|
|
140
|
-
|
141
|
-
|
142
|
-
|
154
|
+
def change
|
155
|
+
%{code}
|
156
|
+
end
|
157
|
+
end"
|
158
|
+
}
|
143
159
|
|
144
|
-
|
145
|
-
|
146
|
-
super.reject { |c| c.name == \"some_column\" }
|
160
|
+
def self.add_check(&block)
|
161
|
+
checks << block
|
147
162
|
end
|
148
163
|
end
|
149
164
|
|
150
|
-
|
151
|
-
|
152
|
-
More info: https://github.com/ankane/strong_migrations#removing-a-column"
|
153
|
-
end
|
165
|
+
ActiveSupport.on_load(:active_record) do
|
166
|
+
ActiveRecord::Migration.prepend(StrongMigrations::Migration)
|
154
167
|
|
155
168
|
if defined?(ActiveRecord::Tasks::DatabaseTasks)
|
156
169
|
ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(StrongMigrations::DatabaseTasks)
|
@@ -14,14 +14,50 @@ module StrongMigrations
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def method_missing(method, *args, &block)
|
17
|
+
table = args[0].to_s
|
18
|
+
|
17
19
|
unless @safe || ENV["SAFETY_ASSURED"] || is_a?(ActiveRecord::Schema) || @direction == :down || version_safe?
|
20
|
+
ar5 = ActiveRecord::VERSION::MAJOR >= 5
|
21
|
+
model = table.classify
|
22
|
+
|
18
23
|
case method
|
19
|
-
when :remove_column
|
20
|
-
|
21
|
-
|
22
|
-
|
24
|
+
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
|
25
|
+
columns =
|
26
|
+
case method
|
27
|
+
when :remove_timestamps
|
28
|
+
["created_at", "updated_at"]
|
29
|
+
when :remove_column
|
30
|
+
[args[1].to_s]
|
31
|
+
when :remove_columns
|
32
|
+
args[1..-1].map(&:to_s)
|
33
|
+
else
|
34
|
+
options = args[2] || {}
|
35
|
+
reference = args[1]
|
36
|
+
cols = []
|
37
|
+
cols << "#{reference}_type" if options[:polymorphic]
|
38
|
+
cols << "#{reference}_id"
|
39
|
+
cols
|
40
|
+
end
|
41
|
+
|
42
|
+
code = ar5 ? "self.ignored_columns = #{columns.inspect}" : "def self.columns\n super.reject { |c| #{columns.inspect}.include?(c.name) }\n end"
|
43
|
+
|
44
|
+
command = String.new("#{method} #{sym_str(table)}")
|
45
|
+
case method
|
46
|
+
when :remove_column, :remove_reference, :remove_belongs_to
|
47
|
+
command << ", #{sym_str(args[1])}#{options_str(args[2] || {})}"
|
48
|
+
when :remove_columns
|
49
|
+
columns.each do |c|
|
50
|
+
command << ", #{sym_str(c)}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raise_error :remove_column,
|
55
|
+
model: model,
|
56
|
+
code: code,
|
57
|
+
command: command,
|
58
|
+
column_suffix: columns.size > 1 ? "s" : ""
|
23
59
|
when :change_table
|
24
|
-
raise_error :change_table
|
60
|
+
raise_error :change_table, header: "Possibly dangerous operation"
|
25
61
|
when :rename_table
|
26
62
|
raise_error :rename_table
|
27
63
|
when :rename_column
|
@@ -30,55 +66,85 @@ module StrongMigrations
|
|
30
66
|
columns = args[1]
|
31
67
|
options = args[2] || {}
|
32
68
|
if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
|
33
|
-
raise_error :add_index_columns
|
69
|
+
raise_error :add_index_columns, header: "Best practice"
|
34
70
|
end
|
35
|
-
if postgresql? && options[:algorithm] != :concurrently && !@new_tables.to_a.include?(
|
36
|
-
raise_error :add_index
|
71
|
+
if postgresql? && options[:algorithm] != :concurrently && !@new_tables.to_a.include?(table)
|
72
|
+
raise_error :add_index,
|
73
|
+
table: sym_str(table),
|
74
|
+
column: column_str(columns),
|
75
|
+
options: options_str(options.except(:algorithm))
|
37
76
|
end
|
38
77
|
when :add_column
|
78
|
+
column = args[1]
|
39
79
|
type = args[2]
|
40
80
|
options = args[3] || {}
|
41
|
-
|
81
|
+
default = options[:default]
|
82
|
+
|
83
|
+
if !default.nil? && !(postgresql? && postgresql_version >= 110000)
|
84
|
+
raise_error :add_column_default,
|
85
|
+
table: sym_str(table),
|
86
|
+
column: sym_str(column),
|
87
|
+
type: sym_str(type),
|
88
|
+
options: options_str(options.except(:default)),
|
89
|
+
default: default.inspect,
|
90
|
+
code: backfill_code(model, column, default)
|
91
|
+
end
|
92
|
+
|
42
93
|
if type.to_s == "json" && postgresql?
|
43
94
|
if postgresql_version >= 90400
|
44
95
|
raise_error :add_column_json
|
45
96
|
else
|
46
|
-
raise_error :add_column_json_legacy
|
97
|
+
raise_error :add_column_json_legacy,
|
98
|
+
model: model,
|
99
|
+
table: connection.quote_table_name(table)
|
47
100
|
end
|
48
101
|
end
|
49
102
|
when :change_column
|
50
103
|
safe = false
|
51
104
|
# assume Postgres 9.1+ since previous versions are EOL
|
52
105
|
if postgresql? && args[2].to_s == "text"
|
53
|
-
column = connection.columns(
|
106
|
+
column = connection.columns(table).find { |c| c.name.to_s == args[1].to_s }
|
54
107
|
safe = column && column.type == :string
|
55
108
|
end
|
56
109
|
raise_error :change_column unless safe
|
57
110
|
when :create_table
|
58
111
|
options = args[1] || {}
|
59
112
|
raise_error :create_table if options[:force]
|
60
|
-
(@new_tables ||= []) <<
|
61
|
-
when :add_reference
|
113
|
+
(@new_tables ||= []) << table
|
114
|
+
when :add_reference, :add_belongs_to
|
62
115
|
options = args[2] || {}
|
63
|
-
index_value = options.fetch(:index,
|
116
|
+
index_value = options.fetch(:index, ar5)
|
64
117
|
if postgresql? && index_value
|
65
|
-
|
118
|
+
reference = args[1]
|
119
|
+
columns = []
|
120
|
+
columns << "#{reference}_type" if options[:polymorphic]
|
121
|
+
columns << "#{reference}_id"
|
122
|
+
raise_error :add_reference,
|
123
|
+
command: method,
|
124
|
+
table: sym_str(table),
|
125
|
+
reference: sym_str(reference),
|
126
|
+
column: column_str(columns),
|
127
|
+
options: options_str(options.except(:index))
|
66
128
|
end
|
67
129
|
when :execute
|
68
|
-
raise_error :execute
|
130
|
+
raise_error :execute, header: "Possibly dangerous operation"
|
69
131
|
when :change_column_null
|
70
|
-
null = args
|
71
|
-
default = args[3]
|
132
|
+
_, column, null, default = args
|
72
133
|
if !null && !default.nil?
|
73
|
-
raise_error :change_column_null
|
134
|
+
raise_error :change_column_null,
|
135
|
+
code: backfill_code(model, column, default)
|
74
136
|
end
|
75
137
|
end
|
138
|
+
|
139
|
+
StrongMigrations.checks.each do |check|
|
140
|
+
instance_exec(method, args, &check)
|
141
|
+
end
|
76
142
|
end
|
77
143
|
|
78
144
|
result = super
|
79
145
|
|
80
146
|
if StrongMigrations.auto_analyze && @direction == :up && postgresql? && method == :add_index
|
81
|
-
connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(
|
147
|
+
connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(table)}"
|
82
148
|
end
|
83
149
|
|
84
150
|
result
|
@@ -98,18 +164,46 @@ module StrongMigrations
|
|
98
164
|
version && version <= StrongMigrations.start_after
|
99
165
|
end
|
100
166
|
|
101
|
-
def raise_error(message_key)
|
102
|
-
wait_message = '
|
103
|
-
__ __ _____ _______ _
|
104
|
-
\ \ / /\ |_ _|__ __| |
|
105
|
-
\ \ /\ / / \ | | | | | |
|
106
|
-
\ \/ \/ / /\ \ | | | | | |
|
107
|
-
\ /\ / ____ \ _| |_ | | |_|
|
108
|
-
\/ \/_/ \_\_____| |_| (_) #strong_migrations
|
109
|
-
|
110
|
-
'
|
167
|
+
def raise_error(message_key, header: nil, **vars)
|
111
168
|
message = StrongMigrations.error_messages[message_key] || "Missing message"
|
112
|
-
|
169
|
+
|
170
|
+
ar5 = ActiveRecord::VERSION::MAJOR >= 5
|
171
|
+
vars[:migration_name] = self.class.name
|
172
|
+
vars[:migration_suffix] = ar5 ? "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" : ""
|
173
|
+
vars[:base_model] = ar5 ? "ApplicationRecord" : "ActiveRecord::Base"
|
174
|
+
|
175
|
+
# escape % not followed by {
|
176
|
+
stop!(message.gsub(/%(?!{)/, "%%") % vars, header: header || "Dangerous operation detected")
|
177
|
+
end
|
178
|
+
|
179
|
+
def sym_str(v)
|
180
|
+
v.to_sym.inspect
|
181
|
+
end
|
182
|
+
|
183
|
+
def column_str(columns)
|
184
|
+
columns = Array(columns).map(&:to_sym)
|
185
|
+
columns = columns.first if columns.size == 1
|
186
|
+
columns.inspect
|
187
|
+
end
|
188
|
+
|
189
|
+
def options_str(options)
|
190
|
+
str = String.new("")
|
191
|
+
options.each do |k, v|
|
192
|
+
str << ", #{k}: #{v.inspect}"
|
193
|
+
end
|
194
|
+
str
|
195
|
+
end
|
196
|
+
|
197
|
+
def backfill_code(model, column, default)
|
198
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
199
|
+
"#{model}.in_batches.update_all #{column}: #{default.inspect}"
|
200
|
+
else
|
201
|
+
"#{model}.find_in_batches do |records|\n #{model}.where(id: records.map(&:id)).update_all #{column}: #{default.inspect}\n end"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def stop!(message, header: "Custom check")
|
206
|
+
raise StrongMigrations::UnsafeMigration, "\n=== #{header} #strong_migrations ===\n\n#{message}\n"
|
113
207
|
end
|
114
208
|
end
|
115
209
|
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.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2018-
|
13
|
+
date: 2018-10-15 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|