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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe026b71dc38ab1cc40189ff8eb858baa45ec0778bd4dd640cff57349aa067ea
4
- data.tar.gz: 28698d9bc1804389fc4e1fdb9e9a3ae9499cc490b15bda953f312fe434a6aa41
3
+ metadata.gz: '08eb5f509b5fb98501eb6f83840c414c331c989b7d92bd29825cbaa0e2312177'
4
+ data.tar.gz: 0fc4b64d44609ff2f6bbbf48d53730e35551654d3a1aaf629b7305ab8ac92caf
5
5
  SHA512:
6
- metadata.gz: 3fe33582e8f6f4c0aced4f162704a9810e044e11e115ac1813892750474c118a33f0fb2ee19883d4dce98bfc6862786eaaf1c7862b4fd3398c2bb032010316fb
7
- data.tar.gz: 0e5bcf7c1a42dcebe09a5f249fd2cb063549c1617d8a161ae915e882ab30e7f9a72b19b38407f5a526b59174cf04bcfa04c790888b7ce069c58c5cf7116faf00
6
+ metadata.gz: fc5cbfa0dbcdc47b8f5e42c18c64d77cabcc01deac0e92077d9d034bff6a47145354da066c70d2eb2906c60c895f5639de40ee9a1c00e015f80d54242f8bcb4c
7
+ data.tar.gz: 5dfcdd45c56da317a83509c6f79c3d81b8c35e3f6e8aaf0af32c8144e134b919da481b2f501a87dbb581f4c61db96bccc4103cf285210f7777f0258020e1c6f3
@@ -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`
@@ -2,17 +2,15 @@
2
2
 
3
3
  First, thanks for wanting to contribute. You’re awesome! :heart:
4
4
 
5
- ## Questions
5
+ ## Help
6
6
 
7
- Use [Stack Overflow](https://stackoverflow.com/) with the tag `strong-migrations`.
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
- ## Feature Requests
9
+ All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
10
10
 
11
- Create an issue. Start the title with `[Idea]`.
11
+ ## Bugs
12
12
 
13
- ## Issues
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 = %w(some_column)
28
+ self.ignored_columns = ["some_column"]
34
29
  end
35
30
 
36
- Once that's deployed, wrap this step in a safety_assured { ... } block.
31
+ Deploy the code, then wrap this step in a safety_assured { ... } block.
37
32
 
38
- More info: https://github.com/ankane/strong_migrations#removing-a-column
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.1]
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.1]
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 |users|
95
- User.where(id: users.map(&:id)).update_all some_column: "default_value"
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 = %w(some_column)
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.1]
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
- If you really have to:
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
- If you really have to:
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.1]
165
+ class AddSomeIndexToUsers < ActiveRecord::Migration[5.2]
164
166
  disable_ddl_transaction!
165
167
 
166
168
  def change
167
- add_index :users, :some_index, algorithm: :concurrently
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
- Check out [this gem](https://github.com/ankane/gindex) to quickly generate index migrations without memorizing the syntax.
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.1]
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.1]
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 [this gem](https://github.com/Shopify/rails-bigint-primarykey).
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
 
@@ -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 entire table to be rewritten.
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 :users, :some_column, :text
25
- change_column_default :users, :some_column, \"default_value\"
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 :users, :some_column
28
+ remove_column %{table}, %{column}
30
29
  end
30
+ end
31
31
 
32
- More info: https://github.com/ankane/strong_migrations#adding-a-column-with-a-default-value",
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
- "Use jsonb instead.",
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
- scope :uniq_on_id, -> { select(\"DISTINCT ON (your_table.id) your_table.*\") }
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
- the entire table and indexes to be rewritten.
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 User < ApplicationRecord
62
- self.ignored_columns = %w(some_column)
71
+ class %{model} < %{base_model}
72
+ %{code}
63
73
  end
64
74
 
65
- Once that's deployed, wrap this step in a safety_assured { ... } block.
75
+ Deploy the code, then wrap this step in a safety_assured { ... } block.
66
76
 
67
- More info: https://github.com/ankane/strong_migrations#removing-a-column",
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
- "If you really have to:
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
- "If you really have to:
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
- add_reference :users, :reference, index: false
96
- add_index :users, :reference_id, algorithm: :concurrently
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 :users, :some_column, algorithm: :concurrently
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
- "The strong_migrations gem does not support inspecting what happens inside a
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
- "The strong_migrations gem does not support inspecting what happens inside an
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
- "The last argument replaces existing NULLs with another value.
130
- This runs a single UPDATE query, which can cause downtime.
131
- Backfill NULLs manually in batches instead.
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
- More info: https://github.com/ankane/strong_migrations#backfilling-data"
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
- if ActiveRecord::VERSION::MAJOR < 5
141
- StrongMigrations.error_messages[:remove_column] = "ActiveRecord caches attributes which causes problems
142
- when removing columns. Be sure to ignore the column:
154
+ def change
155
+ %{code}
156
+ end
157
+ end"
158
+ }
143
159
 
144
- class User < ActiveRecord::Base
145
- def self.columns
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
- Once that's deployed, wrap this step in a safety_assured { ... } block.
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
- raise_error :remove_column
21
- when :remove_timestamps
22
- raise_error :remove_column
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?(args[0].to_s)
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
- raise_error :add_column_default unless options[:default].nil?
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(args[0]).find { |c| c.name.to_s == args[1].to_s }
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 ||= []) << args[0].to_s
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, ActiveRecord::VERSION::MAJOR >= 5 ? true : false)
116
+ index_value = options.fetch(:index, ar5)
64
117
  if postgresql? && index_value
65
- raise_error :add_reference
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[2]
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(args[0])}"
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
- raise StrongMigrations::UnsafeMigration, "#{wait_message}#{message}\n"
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
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  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.3
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-07-23 00:00:00.000000000 Z
13
+ date: 2018-10-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord