strong_migrations 0.6.3 → 0.6.4

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: fee3961cfdf8fe1ad8c3afbf5fa606c142bd647e318c256ec20c7e8d2d34d168
4
- data.tar.gz: db9d38d31fd26ef299f42ef48c2457bef2fcdb29de9a942b007437efeba26b25
3
+ metadata.gz: cefcdafd82baa91c4b9ab160fe502ab6185ae98d324240a2718609736e0a0c58
4
+ data.tar.gz: 1a4a6e6c446fa346df5feeb03f03e102d86d527ac534bd6683a8390e7010010e
5
5
  SHA512:
6
- metadata.gz: 47316b6a2c73fd55fe3ea64a0abd1086d10e959dc10eff0e986b493f221a6a439adc2a65a203c4438a6fc14191864880372cad19561c57177118bad767a35009
7
- data.tar.gz: 89a78de250e9a5fe952d1fc795bc87ec4cce8dd2f442b584a547d48044aa12b0832d7edafc79141afa4e4ca6681ee2f87281bd36548c3b99bf17ecec3845753d
6
+ metadata.gz: 96571822a2dcfdae82d5162a91cf6165d5d1f93b2b554ce0c2fe93fff4e743da3142d4cc124cd0ca2ebd490ce4706c4b9866b8a915cccb74866a0bc629e719eb
7
+ data.tar.gz: df8f8324cf11cbe255dd0b10ef7d89910de265eb45afae39b4ccf587a7064e4e094f630e2ab40045364e2f04266d16afdc019e0666616343619d50f103e2a3a5
@@ -1,3 +1,7 @@
1
+ ## 0.6.4 (2020-04-16)
2
+
3
+ - Added check for `add_reference` with `foreign_key: true`
4
+
1
5
  ## 0.6.3 (2020-04-04)
2
6
 
3
7
  - Increasing precision of `decimal` or `numeric` column is safe in Postgres 9.2+
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Catch unsafe migrations in development
4
4
 
5
+ &nbsp;&nbsp;✓&nbsp;&nbsp;Detects potentially dangerous operations<br />&nbsp;&nbsp;✓&nbsp;&nbsp;Prevents them from running by default<br />&nbsp;&nbsp;✓&nbsp;&nbsp;Provides instructions on safer ways to do what you want
6
+
5
7
  Supports for PostgreSQL, MySQL, and MariaDB
6
8
 
7
9
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
@@ -16,38 +18,36 @@ Add this line to your application’s Gemfile:
16
18
  gem 'strong_migrations'
17
19
  ```
18
20
 
19
- ## How It Works
20
-
21
- 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.
21
+ We highly recommend [setting timeouts](#timeouts). You can [mark existing migrations as safe](#existing-migrations) as well.
22
22
 
23
- ![Screenshot](https://ankane.org/images/strong-migrations.png)
23
+ ## Checks
24
24
 
25
- ## Dangerous Operations
25
+ Potentially dangerous operations:
26
26
 
27
- The following operations can cause downtime or errors:
27
+ - [removing a column](#removing-a-column)
28
+ - [adding a column with a default value](#adding-a-column-with-a-default-value)
29
+ - [backfilling data](#backfilling-data)
30
+ - [changing the type of a column](#renaming-or-changing-the-type-of-a-column)
31
+ - [renaming a column](#renaming-or-changing-the-type-of-a-column)
32
+ - [renaming a table](#renaming-a-table)
33
+ - [creating a table with the force option](#creating-a-table-with-the-force-option)
34
+ - [using change_column_null with a default value](#using-change_column_null-with-a-default-value)
35
+ - [executing SQL directly](#executing-SQL-directly)
28
36
 
29
- - [[+]](#removing-a-column) removing a column
30
- - [[+]](#adding-a-column-with-a-default-value) adding a column with a default value
31
- - [[+]](#backfilling-data) backfilling data
32
- - [[+]](#adding-an-index) adding an index non-concurrently
33
- - [[+]](#adding-a-reference) adding a reference
34
- - [[+]](#adding-a-foreign-key) adding a foreign key
35
- - [[+]](#renaming-or-changing-the-type-of-a-column) changing the type of a column
36
- - [[+]](#renaming-or-changing-the-type-of-a-column) renaming a column
37
- - [[+]](#renaming-a-table) renaming a table
38
- - [[+]](#creating-a-table-with-the-force-option) creating a table with the `force` option
39
- - [[+]](#setting-not-null-on-an-existing-column) setting `NOT NULL` on an existing column
40
- - [[+]](#adding-a-json-column) adding a `json` column
37
+ Postgres-specific checks:
41
38
 
42
- Optional checks:
43
-
44
- - [[+]](#removing-an-index) removing an index non-concurrently
39
+ - [adding an index non-concurrently](#adding-an-index)
40
+ - [removing an index non-concurrently](#removing-an-index)
41
+ - [adding a reference](#adding-a-reference)
42
+ - [adding a foreign key](#adding-a-foreign-key)
43
+ - [adding a json column](#adding-a-json-column)
44
+ - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
45
45
 
46
46
  Best practices:
47
47
 
48
- - [[+]](#keeping-non-unique-indexes-to-three-columns-or-less) keeping non-unique indexes to three columns or less
48
+ - [keeping non-unique indexes to three columns or less](#keeping-non-unique-indexes-to-three-columns-or-less)
49
49
 
50
- ## The Zero Downtime Way
50
+ You can also add [custom checks](#custom-checks) or [disable specific checks](#disable-checks).
51
51
 
52
52
  ### Removing a column
53
53
 
@@ -88,7 +88,7 @@ end
88
88
 
89
89
  ### Adding a column with a default value
90
90
 
91
- Note: This operation is safe in Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+
91
+ Note: This operation is safe in Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+.
92
92
 
93
93
  #### Bad
94
94
 
@@ -155,254 +155,272 @@ class BackfillSomeColumn < ActiveRecord::Migration[6.0]
155
155
  end
156
156
  ```
157
157
 
158
- ### Adding an index
158
+ ### Renaming or changing the type of a column
159
159
 
160
160
  #### Bad
161
161
 
162
- In Postgres, adding an index non-concurrently locks the table.
163
-
164
162
  ```ruby
165
- class AddSomeIndexToUsers < ActiveRecord::Migration[6.0]
163
+ class RenameSomeColumn < ActiveRecord::Migration[6.0]
166
164
  def change
167
- add_index :users, :some_column
165
+ rename_column :users, :some_column, :new_name
168
166
  end
169
167
  end
170
168
  ```
171
169
 
172
- #### Good
173
-
174
- Add indexes concurrently.
170
+ or
175
171
 
176
172
  ```ruby
177
- class AddSomeIndexToUsers < ActiveRecord::Migration[6.0]
178
- disable_ddl_transaction!
179
-
173
+ class ChangeSomeColumnType < ActiveRecord::Migration[6.0]
180
174
  def change
181
- add_index :users, :some_column, algorithm: :concurrently
175
+ change_column :users, :some_column, :new_type
182
176
  end
183
177
  end
184
178
  ```
185
179
 
186
- 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.
180
+ A few changes are safe in Postgres:
187
181
 
188
- With [gindex](https://github.com/ankane/gindex), you can generate an index migration instantly with:
182
+ - Changing between `varchar` and `text` columns
183
+ - Increasing the precision of a `decimal` or `numeric` column
184
+ - Making a `decimal` or `numeric` column unconstrained
185
+ - Changing between `timestamp` and `timestamptz` columns when session time zone is UTC in Postgres 12+
189
186
 
190
- ```sh
191
- rails g index table column
192
- ```
187
+ And a few in MySQL and MariaDB:
193
188
 
194
- ### Adding a reference
189
+ - Increasing the length of a `varchar` column from under 255 up to 255
190
+ - Increasing the length of a `varchar` column over 255
195
191
 
196
- #### Bad
192
+ #### Good
197
193
 
198
- Rails adds an index non-concurrently to references by default, which is problematic for Postgres.
194
+ A safer approach is to:
195
+
196
+ 1. Create a new column
197
+ 2. Write to both columns
198
+ 3. Backfill data from the old column to the new column
199
+ 4. Move reads from the old column to the new column
200
+ 5. Stop writing to the old column
201
+ 6. Drop the old column
202
+
203
+ ### Renaming a table
204
+
205
+ #### Bad
199
206
 
200
207
  ```ruby
201
- class AddReferenceToUsers < ActiveRecord::Migration[6.0]
208
+ class RenameUsersToCustomers < ActiveRecord::Migration[6.0]
202
209
  def change
203
- add_reference :users, :city
210
+ rename_table :users, :customers
204
211
  end
205
212
  end
206
213
  ```
207
214
 
208
215
  #### Good
209
216
 
210
- Make sure the index is added concurrently.
211
-
212
- ```ruby
213
- class AddReferenceToUsers < ActiveRecord::Migration[6.0]
214
- disable_ddl_transaction!
217
+ A safer approach is to:
215
218
 
216
- def change
217
- add_reference :users, :city, index: {algorithm: :concurrently}
218
- end
219
- end
220
- ```
219
+ 1. Create a new table
220
+ 2. Write to both tables
221
+ 3. Backfill data from the old table to new table
222
+ 4. Move reads from the old table to the new table
223
+ 5. Stop writing to the old table
224
+ 6. Drop the old table
221
225
 
222
- ### Adding a foreign key
226
+ ### Creating a table with the force option
223
227
 
224
228
  #### Bad
225
229
 
226
- In Postgres, new foreign keys are validated by default, which acquires a `ShareRowExclusiveLock` that can be [expensive on large tables](https://travisofthenorth.com/blog/2017/2/2/postgres-adding-foreign-keys-with-zero-downtime).
230
+ The `force` option can drop an existing table.
227
231
 
228
232
  ```ruby
229
- class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0]
233
+ class CreateUsers < ActiveRecord::Migration[6.0]
230
234
  def change
231
- add_foreign_key :users, :orders
235
+ create_table :users, force: true do |t|
236
+ # ...
237
+ end
232
238
  end
233
239
  end
234
240
  ```
235
241
 
236
242
  #### Good
237
243
 
238
- Instead, validate it in a separate migration with a more agreeable `RowShareLock`. This approach is documented by Postgres to have “[the least impact on other work](https://www.postgresql.org/docs/current/sql-altertable.html).”
239
-
240
- For Rails 5.2+, use:
244
+ Create tables without the `force` option.
241
245
 
242
246
  ```ruby
243
- class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0]
247
+ class CreateUsers < ActiveRecord::Migration[6.0]
244
248
  def change
245
- add_foreign_key :users, :orders, validate: false
249
+ create_table :users do |t|
250
+ # ...
251
+ end
246
252
  end
247
253
  end
248
254
  ```
249
255
 
250
- Then validate it in a separate migration.
256
+ ### Using change_column_null with a default value
257
+
258
+ #### Bad
259
+
260
+ This generates a single `UPDATE` statement to set the default value.
251
261
 
252
262
  ```ruby
253
- class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.0]
263
+ class ChangeSomeColumnNull < ActiveRecord::Migration[6.0]
254
264
  def change
255
- validate_foreign_key :users, :orders
265
+ change_column_null :users, :some_column, false, "default_value"
256
266
  end
257
267
  end
258
268
  ```
259
269
 
260
- For Rails < 5.2, use:
270
+ #### Good
271
+
272
+ Backfill the column [safely](#backfilling-data). Then use:
261
273
 
262
274
  ```ruby
263
- class AddForeignKeyOnUsers < ActiveRecord::Migration[5.1]
275
+ class ChangeSomeColumnNull < ActiveRecord::Migration[6.0]
264
276
  def change
265
- safety_assured do
266
- execute 'ALTER TABLE "users" ADD CONSTRAINT "fk_rails_c1e9b98e31" FOREIGN KEY ("order_id") REFERENCES "orders" ("id") NOT VALID'
267
- end
277
+ change_column_null :users, :some_column, false
268
278
  end
269
279
  end
270
280
  ```
271
281
 
272
- Then validate it in a separate migration.
282
+ Note: In Postgres, `change_column_null` is still [not safe](#setting-not-null-on-an-existing-column) with this method.
283
+
284
+ ### Executing SQL directly
285
+
286
+ Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
273
287
 
274
288
  ```ruby
275
- class ValidateForeignKeyOnUsers < ActiveRecord::Migration[5.1]
289
+ class ExecuteSQL < ActiveRecord::Migration[6.0]
276
290
  def change
277
- safety_assured do
278
- execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "fk_rails_c1e9b98e31"'
279
- end
291
+ safety_assured { execute "..." }
280
292
  end
281
293
  end
282
294
  ```
283
295
 
284
- ### Renaming or changing the type of a column
296
+ ### Adding an index
285
297
 
286
298
  #### Bad
287
299
 
300
+ In Postgres, adding an index non-concurrently locks the table.
301
+
288
302
  ```ruby
289
- class RenameSomeColumn < ActiveRecord::Migration[6.0]
303
+ class AddSomeIndexToUsers < ActiveRecord::Migration[6.0]
290
304
  def change
291
- rename_column :users, :some_column, :new_name
305
+ add_index :users, :some_column
292
306
  end
293
307
  end
294
308
  ```
295
309
 
296
- or
310
+ #### Good
311
+
312
+ Add indexes concurrently.
297
313
 
298
314
  ```ruby
299
- class ChangeSomeColumnType < ActiveRecord::Migration[6.0]
315
+ class AddSomeIndexToUsers < ActiveRecord::Migration[6.0]
316
+ disable_ddl_transaction!
317
+
300
318
  def change
301
- change_column :users, :some_column, :new_type
319
+ add_index :users, :some_column, algorithm: :concurrently
302
320
  end
303
321
  end
304
322
  ```
305
323
 
306
- A few changes are safe in Postgres:
307
-
308
- - Changing between `varchar` and `text` columns
309
- - Increasing the precision of a `decimal` or `numeric` column
310
- - Making a `decimal` or `numeric` column unconstrained
311
- - Changing between `timestamp` and `timestamptz` columns when session time zone is UTC in Postgres 12+
312
-
313
- And a few in MySQL and MariaDB:
314
-
315
- - Increasing the length of a `varchar` column from under 255 up to 255
316
- - Increasing the length of a `varchar` column over 255
324
+ 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.
317
325
 
318
- #### Good
326
+ With [gindex](https://github.com/ankane/gindex), you can generate an index migration instantly with:
319
327
 
320
- A safer approach is to:
328
+ ```sh
329
+ rails g index table column
330
+ ```
321
331
 
322
- 1. Create a new column
323
- 2. Write to both columns
324
- 3. Backfill data from the old column to the new column
325
- 4. Move reads from the old column to the new column
326
- 5. Stop writing to the old column
327
- 6. Drop the old column
332
+ ### Removing an index
328
333
 
329
- ### Renaming a table
334
+ Note: This check is [opt-in](#opt-in-checks).
330
335
 
331
336
  #### Bad
332
337
 
338
+ In Postgres, removing an index non-concurrently locks the table for a brief period.
339
+
333
340
  ```ruby
334
- class RenameUsersToCustomers < ActiveRecord::Migration[6.0]
341
+ class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0]
335
342
  def change
336
- rename_table :users, :customers
343
+ remove_index :users, :some_column
337
344
  end
338
345
  end
339
346
  ```
340
347
 
341
348
  #### Good
342
349
 
343
- A safer approach is to:
350
+ Remove indexes concurrently.
344
351
 
345
- 1. Create a new table
346
- 2. Write to both tables
347
- 3. Backfill data from the old table to new table
348
- 4. Move reads from the old table to the new table
349
- 5. Stop writing to the old table
350
- 6. Drop the old table
352
+ ```ruby
353
+ class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0]
354
+ disable_ddl_transaction!
351
355
 
352
- ### Creating a table with the force option
356
+ def change
357
+ remove_index :users, column: :some_column, algorithm: :concurrently
358
+ end
359
+ end
360
+ ```
361
+
362
+ ### Adding a reference
353
363
 
354
364
  #### Bad
355
365
 
356
- The `force` option can drop an existing table.
366
+ Rails adds an index non-concurrently to references by default, which is problematic for Postgres.
357
367
 
358
368
  ```ruby
359
- class CreateUsers < ActiveRecord::Migration[6.0]
369
+ class AddReferenceToUsers < ActiveRecord::Migration[6.0]
360
370
  def change
361
- create_table :users, force: true do |t|
362
- # ...
363
- end
371
+ add_reference :users, :city
364
372
  end
365
373
  end
366
374
  ```
367
375
 
368
376
  #### Good
369
377
 
370
- Create tables without the `force` option.
378
+ Make sure the index is added concurrently.
371
379
 
372
380
  ```ruby
373
- class CreateUsers < ActiveRecord::Migration[6.0]
381
+ class AddReferenceToUsers < ActiveRecord::Migration[6.0]
382
+ disable_ddl_transaction!
383
+
374
384
  def change
375
- create_table :users do |t|
376
- # ...
377
- end
385
+ add_reference :users, :city, index: {algorithm: :concurrently}
378
386
  end
379
387
  end
380
388
  ```
381
389
 
382
- ### Setting `NOT NULL` on an existing column
390
+ ### Adding a foreign key
383
391
 
384
392
  #### Bad
385
393
 
386
- In Postgres, setting `NOT NULL` on an existing column requires an `AccessExclusiveLock`, which is expensive on large tables.
394
+ In Postgres, new foreign keys are validated by default, which acquires a `ShareRowExclusiveLock` that can be [expensive on large tables](https://travisofthenorth.com/blog/2017/2/2/postgres-adding-foreign-keys-with-zero-downtime).
387
395
 
388
396
  ```ruby
389
- class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
397
+ class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0]
390
398
  def change
391
- change_column_null :users, :some_column, false
399
+ add_foreign_key :users, :orders
400
+ end
401
+ end
402
+ ```
403
+
404
+ or
405
+
406
+ ```ruby
407
+ class AddReferenceToUsers < ActiveRecord::Migration[6.0]
408
+ def change
409
+ add_reference :users, :order, foreign_key: true
392
410
  end
393
411
  end
394
412
  ```
395
413
 
396
414
  #### Good
397
415
 
398
- Instead, add a constraint:
416
+ Instead, validate it in a separate migration with a more agreeable `RowShareLock`. This approach is documented by Postgres to have “[the least impact on other work](https://www.postgresql.org/docs/current/sql-altertable.html).”
417
+
418
+ For Rails 5.2+, use:
399
419
 
400
420
  ```ruby
401
- class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
421
+ class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0]
402
422
  def change
403
- safety_assured do
404
- execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
405
- end
423
+ add_foreign_key :users, :orders, validate: false
406
424
  end
407
425
  end
408
426
  ```
@@ -410,45 +428,37 @@ end
410
428
  Then validate it in a separate migration.
411
429
 
412
430
  ```ruby
413
- class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
431
+ class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.0]
414
432
  def change
415
- safety_assured do
416
- execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
417
- end
433
+ validate_foreign_key :users, :orders
418
434
  end
419
435
  end
420
436
  ```
421
437
 
422
- Note: This is not 100% the same as `NOT NULL` column constraint. Here’s a [good explanation](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c).
423
-
424
- ### Using change_column_null with a default value
425
-
426
- #### Bad
427
-
428
- This generates a single `UPDATE` statement to set the default value.
438
+ For Rails < 5.2, use:
429
439
 
430
440
  ```ruby
431
- class ChangeSomeColumnNull < ActiveRecord::Migration[6.0]
441
+ class AddForeignKeyOnUsers < ActiveRecord::Migration[5.1]
432
442
  def change
433
- change_column_null :users, :some_column, false, "default_value"
443
+ safety_assured do
444
+ execute 'ALTER TABLE "users" ADD CONSTRAINT "fk_rails_c1e9b98e31" FOREIGN KEY ("order_id") REFERENCES "orders" ("id") NOT VALID'
445
+ end
434
446
  end
435
447
  end
436
448
  ```
437
449
 
438
- #### Good
439
-
440
- Backfill the column [safely](#backfilling-data). Then use:
450
+ Then validate it in a separate migration.
441
451
 
442
452
  ```ruby
443
- class ChangeSomeColumnNull < ActiveRecord::Migration[6.0]
453
+ class ValidateForeignKeyOnUsers < ActiveRecord::Migration[5.1]
444
454
  def change
445
- change_column_null :users, :some_column, false
455
+ safety_assured do
456
+ execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "fk_rails_c1e9b98e31"'
457
+ end
446
458
  end
447
459
  end
448
460
  ```
449
461
 
450
- Note: In Postgres, `change_column_null` is still [not safe](#setting-not-null-on-an-existing-column) with this method.
451
-
452
462
  ### Adding a json column
453
463
 
454
464
  #### Bad
@@ -475,49 +485,47 @@ class AddPropertiesToUsers < ActiveRecord::Migration[6.0]
475
485
  end
476
486
  ```
477
487
 
478
- ## Optional Checks
479
-
480
- Some operations rarely cause issues in practice, but can be checked if desired. Enable checks with:
488
+ ### Setting NOT NULL on an existing column
481
489
 
482
- ```ruby
483
- StrongMigrations.enable_check(:remove_index)
484
- ```
490
+ #### Bad
485
491
 
486
- To start a check only after a specific migration, use:
492
+ In Postgres, setting `NOT NULL` on an existing column requires an `AccessExclusiveLock`, which is expensive on large tables.
487
493
 
488
494
  ```ruby
489
- StrongMigrations.enable_check(:remove_index, start_after: 20170101000000)
495
+ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
496
+ def change
497
+ change_column_null :users, :some_column, false
498
+ end
499
+ end
490
500
  ```
491
501
 
492
- ### Removing an index
493
-
494
- #### Bad
502
+ #### Good
495
503
 
496
- In Postgres, removing an index non-concurrently locks the table for a brief period.
504
+ Instead, add a constraint:
497
505
 
498
506
  ```ruby
499
- class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0]
507
+ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
500
508
  def change
501
- remove_index :users, :some_column
509
+ safety_assured do
510
+ execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
511
+ end
502
512
  end
503
513
  end
504
514
  ```
505
515
 
506
- #### Good
507
-
508
- Remove indexes concurrently.
516
+ Then validate it in a separate migration.
509
517
 
510
518
  ```ruby
511
- class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0]
512
- disable_ddl_transaction!
513
-
519
+ class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
514
520
  def change
515
- remove_index :users, column: :some_column, algorithm: :concurrently
521
+ safety_assured do
522
+ execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
523
+ end
516
524
  end
517
525
  end
518
526
  ```
519
527
 
520
- ## Best Practices
528
+ Note: This is not 100% the same as `NOT NULL` column constraint. Here’s a [good explanation](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c).
521
529
 
522
530
  ### Keeping non-unique indexes to three columns or less
523
531
 
@@ -577,6 +585,20 @@ Use the `stop!` method to stop migrations.
577
585
 
578
586
  Note: Since `remove_column` always requires a `safety_assured` block, it’s not possible to add a custom check for `remove_column` operations.
579
587
 
588
+ ## Opt-in Checks
589
+
590
+ Some operations rarely cause issues in practice, but can be checked if desired. Enable checks with:
591
+
592
+ ```ruby
593
+ StrongMigrations.enable_check(:remove_index)
594
+ ```
595
+
596
+ To start a check only after a specific migration, use:
597
+
598
+ ```ruby
599
+ StrongMigrations.enable_check(:remove_index, start_after: 20170101000000)
600
+ ```
601
+
580
602
  ## Disable Checks
581
603
 
582
604
  Disable specific checks with:
@@ -587,50 +609,57 @@ StrongMigrations.disable_check(:add_index)
587
609
 
588
610
  Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
589
611
 
590
- ## Existing Migrations
612
+ ## Custom Messages
591
613
 
592
- To mark migrations as safe that were created before installing this gem, create an initializer with:
614
+ To customize specific messages, create an initializer with:
593
615
 
594
616
  ```ruby
595
- StrongMigrations.start_after = 20170101000000
617
+ StrongMigrations.error_messages[:add_column_default] = "Your custom instructions"
596
618
  ```
597
619
 
598
- Use the version from your latest migration.
620
+ Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
599
621
 
600
- ## Dangerous Tasks
622
+ ## Timeouts
601
623
 
602
- For safety, dangerous database tasks are disabled in production - `db:drop`, `db:reset`, `db:schema:load`, and `db:structure:load`. To get around this, use:
624
+ It’s a good idea to set a long statement timeout and a short lock timeout for migrations. This way, migrations can run for a while, and if a migration can’t acquire a lock in a timely manner, other statements won’t be stuck behind it.
603
625
 
604
- ```sh
605
- SAFETY_ASSURED=1 rails db:drop
606
- ```
626
+ Create `config/initializers/strong_migrations.rb` with:
607
627
 
608
- ## Faster Migrations
628
+ ```ruby
629
+ StrongMigrations.statement_timeout = 1.hour
630
+ StrongMigrations.lock_timeout = 10.seconds
631
+ ```
609
632
 
610
- Only dump the schema when adding a new migration. If you use Git, create an initializer with:
633
+ Or set the timeouts directly on the database user that runs migrations. For Postgres, use:
611
634
 
612
- ```ruby
613
- ActiveRecord::Base.dump_schema_after_migration = Rails.env.development? &&
614
- `git status db/migrate/ --porcelain`.present?
635
+ ```sql
636
+ ALTER ROLE myuser SET statement_timeout = '1h';
637
+ ALTER ROLE myuser SET lock_timeout = '10s';
615
638
  ```
616
639
 
617
- ## Schema Sanity
640
+ Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
618
641
 
619
- Columns can flip order in `db/schema.rb` when you have multiple developers. One way to prevent this is to [alphabetize them](https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/). Add to the end of your `Rakefile`:
642
+ ## Existing Migrations
643
+
644
+ To mark migrations as safe that were created before installing this gem, create an initializer with:
620
645
 
621
646
  ```ruby
622
- task "db:schema:dump": "strong_migrations:alphabetize_columns"
647
+ StrongMigrations.start_after = 20170101000000
623
648
  ```
624
649
 
625
- ## Custom Messages
650
+ Use the version from your latest migration.
626
651
 
627
- To customize specific messages, create an initializer with:
652
+ ## Target Version
653
+
654
+ If your development database version is different from production, you can specify the production version so the right checks are run in development.
628
655
 
629
656
  ```ruby
630
- StrongMigrations.error_messages[:add_column_default] = "Your custom instructions"
657
+ StrongMigrations.target_postgresql_version = "10"
658
+ StrongMigrations.target_mysql_version = "8.0.12"
659
+ StrongMigrations.target_mariadb_version = "10.3.2"
631
660
  ```
632
661
 
633
- Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
662
+ For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
634
663
 
635
664
  ## Analyze Tables
636
665
 
@@ -640,37 +669,30 @@ Analyze tables automatically (to update planner statistics) after an index is ad
640
669
  StrongMigrations.auto_analyze = true
641
670
  ```
642
671
 
643
- ## Target Version
672
+ ## Faster Migrations
644
673
 
645
- If your development database version is different from production, you can specify the production version so the right checks are run in development.
674
+ Only dump the schema when adding a new migration. If you use Git, create an initializer with:
646
675
 
647
676
  ```ruby
648
- StrongMigrations.target_postgresql_version = "10"
649
- StrongMigrations.target_mysql_version = "8.0.12"
650
- StrongMigrations.target_mariadb_version = "10.3.2"
677
+ ActiveRecord::Base.dump_schema_after_migration = Rails.env.development? &&
678
+ `git status db/migrate/ --porcelain`.present?
651
679
  ```
652
680
 
653
- For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
654
-
655
- ## Timeouts
656
-
657
- It’s a good idea to set a long statement timeout and a short lock timeout for migrations. This way, migrations can run for a while, and if a migration can’t acquire a lock in a timely manner, other statements won’t be stuck behind it.
681
+ ## Schema Sanity
658
682
 
659
- You can use:
683
+ Columns can flip order in `db/schema.rb` when you have multiple developers. One way to prevent this is to [alphabetize them](https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/). Add to the end of your `Rakefile`:
660
684
 
661
685
  ```ruby
662
- StrongMigrations.statement_timeout = 1.hour
663
- StrongMigrations.lock_timeout = 10.seconds
686
+ task "db:schema:dump": "strong_migrations:alphabetize_columns"
664
687
  ```
665
688
 
666
- Or set the timeouts directly on the database user that runs migrations. For Postgres, use:
689
+ ## Dangerous Tasks
667
690
 
668
- ```sql
669
- ALTER ROLE myuser SET statement_timeout = '1h';
670
- ALTER ROLE myuser SET lock_timeout = '10s';
671
- ```
691
+ For safety, dangerous database tasks are disabled in production - `db:drop`, `db:reset`, `db:schema:load`, and `db:structure:load`. To get around this, use:
672
692
 
673
- Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
693
+ ```sh
694
+ SAFETY_ASSURED=1 rails db:drop
695
+ ```
674
696
 
675
697
  ## Permissions
676
698
 
@@ -47,7 +47,7 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
47
47
  def up
48
48
  %{code}
49
49
  end
50
- end%{append}",
50
+ end",
51
51
 
52
52
  add_column_json:
53
53
  "There's no equality operator for the json column type, which can
@@ -100,7 +100,7 @@ end",
100
100
  6. Drop the old table",
101
101
 
102
102
  add_reference:
103
- "Adding an index non-concurrently locks the table. Instead, use:
103
+ "%{headline} Instead, use:
104
104
 
105
105
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
106
106
  disable_ddl_transaction!
@@ -86,7 +86,7 @@ module StrongMigrations
86
86
  options = options.except(:null)
87
87
  append = "
88
88
 
89
- Then add the NOT NULL constraint."
89
+ Then add the NOT NULL constraint in separate migrations."
90
90
  end
91
91
 
92
92
  raise_error :add_column_default,
@@ -159,19 +159,34 @@ Then add the NOT NULL constraint."
159
159
  table, reference, options = args
160
160
  options ||= {}
161
161
 
162
- index_value = options.fetch(:index, true)
163
- concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently
162
+ if postgresql?
163
+ index_value = options.fetch(:index, true)
164
+ concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently
165
+ bad_index = index_value && !concurrently_set
164
166
 
165
- if postgresql? && index_value && !concurrently_set
166
- columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
167
+ if bad_index || options[:foreign_key]
168
+ columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
167
169
 
168
- if index_value.is_a?(Hash)
169
- options[:index] = options[:index].merge(algorithm: :concurrently)
170
- else
171
- options = options.merge(index: {algorithm: :concurrently})
172
- end
170
+ if index_value.is_a?(Hash)
171
+ options[:index] = options[:index].merge(algorithm: :concurrently)
172
+ else
173
+ options = options.merge(index: {algorithm: :concurrently})
174
+ end
175
+
176
+ if options.delete(:foreign_key)
177
+ headline = "Adding a validated foreign key locks the table."
178
+ append = "
173
179
 
174
- raise_error :add_reference, command: command_str(method, [table, reference, options])
180
+ Then add the foreign key in separate migrations."
181
+ else
182
+ headline = "Adding an index non-concurrently locks the table."
183
+ end
184
+
185
+ raise_error :add_reference,
186
+ headline: headline,
187
+ command: command_str(method, [table, reference, options]),
188
+ append: append
189
+ end
175
190
  end
176
191
  when :execute
177
192
  raise_error :execute, header: "Possibly dangerous operation"
@@ -347,20 +362,16 @@ Then add the NOT NULL constraint."
347
362
  StrongMigrations.helpers
348
363
  end
349
364
 
350
- def raise_error(message_key, header: nil, **vars)
365
+ def raise_error(message_key, header: nil, append: nil, **vars)
351
366
  return unless StrongMigrations.check_enabled?(message_key, version: version)
352
367
 
353
368
  message = StrongMigrations.error_messages[message_key] || "Missing message"
369
+ message = message + append if append
354
370
 
355
371
  vars[:migration_name] = @migration.class.name
356
372
  vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
357
373
  vars[:base_model] = "ApplicationRecord"
358
374
 
359
- # interpolate variables in appended code
360
- if vars[:append]
361
- vars[:append] = vars[:append].gsub(/%(?!{)/, "%%") % vars
362
- end
363
-
364
375
  # escape % not followed by {
365
376
  message = message.gsub(/%(?!{)/, "%%") % vars if message.include?("%")
366
377
  @migration.stop!(message, header: header || "Dangerous operation detected")
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.6.3"
2
+ VERSION = "0.6.4"
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.6.3
4
+ version: 0.6.4
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: 2020-04-04 00:00:00.000000000 Z
13
+ date: 2020-04-16 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord