strong_migrations 0.2.3 → 0.3.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 +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
|