strong_migrations 0.6.3 → 0.6.4

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 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