strong_migrations 0.6.8 → 0.7.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: f64d60856479de1a9071b64be2186256e619c30a6caea46d2f8094fb1d3ded4b
4
- data.tar.gz: 586c15c10b53ba6273685d3d4bbeaa84f0c23fc973643abac1a2687d3ad2e986
3
+ metadata.gz: 2bd2b26a8cb5d3577ffaad3878ff961dec2bc6cb55c51897b2681febbf8f0b10
4
+ data.tar.gz: b48c9ff1050345351a9f938c6dd6dc8530fbc4842e0fc3c60702287fa3fb825c
5
5
  SHA512:
6
- metadata.gz: d5aac390d0d1f965819311ec4ad0cc18b66eb310768f5ed95635670ca646f8804cf0fb672c94f4f1b59f6d812b027cc2d666607bf9b55d5a5f58f37726056d8c
7
- data.tar.gz: f0ec6e96d589aa99df461bbcc29201142a25a74a951edbbdf72c49e8a1d81a58623ccb6f049d2630a5cbd517f9edf9119511087d51da90f95c9794a13f5c1fb1
6
+ metadata.gz: 4bc2817b7878c3041442e4e4607db55b2861e7cba16220e635ee28a4b9ebd6dbd0a7ab16033555fa51f4f98601fe0eda198fa91e3030dfff63839c7cdc9aa028
7
+ data.tar.gz: 254e72cd2ce295759819f05cde920cf4c3ea9ae0d45af9f21d6ac11d55549d0d3544f97f511dc5b1dc0cf44c03c25af4208e4ab4f317449d9aabea90cd485b99
@@ -1,3 +1,30 @@
1
+ ## 0.7.4 (2020-12-16)
2
+
3
+ - Added `safe_by_default` option to install generator
4
+ - Fixed warnings with Active Record 6.1
5
+
6
+ ## 0.7.3 (2020-11-24)
7
+
8
+ - Added `safe_by_default` option
9
+
10
+ ## 0.7.2 (2020-10-25)
11
+
12
+ - Added support for float timeouts
13
+
14
+ ## 0.7.1 (2020-07-27)
15
+
16
+ - Added `target_version` option to replace database-specific options
17
+
18
+ ## 0.7.0 (2020-07-22)
19
+
20
+ - Added `check_down` option
21
+ - Added check for `change_column` with `null: false`
22
+ - Added check for `validate_foreign_key`
23
+ - Improved error messages
24
+ - Made auto analyze less verbose in Postgres
25
+ - Decreasing the length limit of a `varchar` column or adding a limit is not safe in Postgres
26
+ - Removed safety checks for `db` rake tasks (Rails 5+ handles this)
27
+
1
28
  ## 0.6.8 (2020-05-13)
2
29
 
3
30
  - `change_column_null` on a column with a `NOT NULL` constraint is safe in Postgres 12+
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Bob Remeika and David Waller, 2015-2019 Andrew Kane
1
+ Copyright (c) 2013 Bob Remeika and David Waller, 2015-2020 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -8,7 +8,7 @@ Supports for PostgreSQL, MySQL, and MariaDB
8
8
 
9
9
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
10
10
 
11
- [![Build Status](https://travis-ci.org/ankane/strong_migrations.svg?branch=master)](https://travis-ci.org/ankane/strong_migrations)
11
+ [![Build Status](https://github.com/ankane/strong_migrations/workflows/build/badge.svg?branch=master)](https://github.com/ankane/strong_migrations/actions)
12
12
 
13
13
  ## Installation
14
14
 
@@ -25,6 +25,36 @@ bundle install
25
25
  rails generate strong_migrations:install
26
26
  ```
27
27
 
28
+ Strong Migrations sets a long statement timeout for migrations so you can set a [short statement timeout](#app-timeouts) for your application.
29
+
30
+ ## How It Works
31
+
32
+ When you run a migration that’s potentially dangerous, you’ll see an error message like:
33
+
34
+ ```txt
35
+ === Dangerous operation detected #strong_migrations ===
36
+
37
+ Active Record caches attributes, which causes problems
38
+ when removing columns. Be sure to ignore the column:
39
+
40
+ class User < ApplicationRecord
41
+ self.ignored_columns = ["name"]
42
+ end
43
+
44
+ Deploy the code, then wrap this step in a safety_assured { ... } block.
45
+
46
+ class RemoveColumn < ActiveRecord::Migration[6.0]
47
+ def change
48
+ safety_assured { remove_column :users, :name }
49
+ end
50
+ end
51
+ ```
52
+
53
+ An operation is classified as dangerous if it either:
54
+
55
+ - Blocks reads or writes for more than a few seconds (after a lock is acquired)
56
+ - Has a good chance of causing application errors
57
+
28
58
  ## Checks
29
59
 
30
60
  Potentially dangerous operations:
@@ -32,11 +62,11 @@ Potentially dangerous operations:
32
62
  - [removing a column](#removing-a-column)
33
63
  - [adding a column with a default value](#adding-a-column-with-a-default-value)
34
64
  - [backfilling data](#backfilling-data)
35
- - [changing the type of a column](#renaming-or-changing-the-type-of-a-column)
36
- - [renaming a column](#renaming-or-changing-the-type-of-a-column)
65
+ - [changing the type of a column](#changing-the-type-of-a-column)
66
+ - [renaming a column](#renaming-a-column)
37
67
  - [renaming a table](#renaming-a-table)
38
68
  - [creating a table with the force option](#creating-a-table-with-the-force-option)
39
- - [using change_column_null with a default value](#using-change_column_null-with-a-default-value)
69
+ - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
40
70
  - [executing SQL directly](#executing-SQL-directly)
41
71
 
42
72
  Postgres-specific checks:
@@ -45,7 +75,6 @@ Postgres-specific checks:
45
75
  - [adding a reference](#adding-a-reference)
46
76
  - [adding a foreign key](#adding-a-foreign-key)
47
77
  - [adding a json column](#adding-a-json-column)
48
- - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
49
78
 
50
79
  Best practices:
51
80
 
@@ -57,7 +86,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
57
86
 
58
87
  #### Bad
59
88
 
60
- ActiveRecord caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
89
+ Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
61
90
 
62
91
  ```ruby
63
92
  class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.0]
@@ -69,7 +98,7 @@ end
69
98
 
70
99
  #### Good
71
100
 
72
- 1. Tell ActiveRecord to ignore the column from its cache
101
+ 1. Tell Active Record to ignore the column from its cache
73
102
 
74
103
  ```ruby
75
104
  class User < ApplicationRecord
@@ -92,11 +121,9 @@ end
92
121
 
93
122
  ### Adding a column with a default value
94
123
 
95
- Note: This operation is safe in Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+.
96
-
97
124
  #### Bad
98
125
 
99
- Adding a column with a default value to an existing table causes the entire table to be rewritten.
126
+ In earlier versions of Postgres, MySQL, and MariaDB, adding a column with a default value to an existing table causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
100
127
 
101
128
  ```ruby
102
129
  class AddSomeColumnToUsers < ActiveRecord::Migration[6.0]
@@ -106,6 +133,8 @@ class AddSomeColumnToUsers < ActiveRecord::Migration[6.0]
106
133
  end
107
134
  ```
108
135
 
136
+ In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a table rewrite and is safe.
137
+
109
138
  #### Good
110
139
 
111
140
  Instead, add the column without a default value, then change the default.
@@ -129,7 +158,7 @@ See the next section for how to backfill.
129
158
 
130
159
  #### Bad
131
160
 
132
- Backfilling in the same transaction that alters a table locks the table for the [duration of the backfill](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/).
161
+ Active Record creates a transaction around each migration, and backfilling in the same transaction that alters a table keeps the table locked for the [duration of the backfill](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/).
133
162
 
134
163
  ```ruby
135
164
  class AddSomeColumnToUsers < ActiveRecord::Migration[6.0]
@@ -159,19 +188,11 @@ class BackfillSomeColumn < ActiveRecord::Migration[6.0]
159
188
  end
160
189
  ```
161
190
 
162
- ### Renaming or changing the type of a column
191
+ ### Changing the type of a column
163
192
 
164
193
  #### Bad
165
194
 
166
- ```ruby
167
- class RenameSomeColumn < ActiveRecord::Migration[6.0]
168
- def change
169
- rename_column :users, :some_column, :new_name
170
- end
171
- end
172
- ```
173
-
174
- or
195
+ Changing the type of a column causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
175
196
 
176
197
  ```ruby
177
198
  class ChangeSomeColumnType < ActiveRecord::Migration[6.0]
@@ -181,17 +202,44 @@ class ChangeSomeColumnType < ActiveRecord::Migration[6.0]
181
202
  end
182
203
  ```
183
204
 
184
- A few changes are safe in Postgres:
205
+ A few changes don’t require a table rewrite (and are safe) in Postgres:
185
206
 
186
- - Changing between `varchar` and `text` columns
207
+ - Increasing the length limit of a `varchar` column (or removing the limit)
208
+ - Changing a `varchar` column to a `text` column
209
+ - Changing a `text` column to a `varchar` column with no length limit
187
210
  - Increasing the precision of a `decimal` or `numeric` column
188
211
  - Making a `decimal` or `numeric` column unconstrained
189
212
  - Changing between `timestamp` and `timestamptz` columns when session time zone is UTC in Postgres 12+
190
213
 
191
214
  And a few in MySQL and MariaDB:
192
215
 
193
- - Increasing the length of a `varchar` column from under 255 up to 255
194
- - Increasing the length of a `varchar` column over 255
216
+ - Increasing the length limit of a `varchar` column from under 255 up to 255
217
+ - Increasing the length limit of a `varchar` column from over 255 to the max limit
218
+
219
+ #### Good
220
+
221
+ A safer approach is to:
222
+
223
+ 1. Create a new column
224
+ 2. Write to both columns
225
+ 3. Backfill data from the old column to the new column
226
+ 4. Move reads from the old column to the new column
227
+ 5. Stop writing to the old column
228
+ 6. Drop the old column
229
+
230
+ ### Renaming a column
231
+
232
+ #### Bad
233
+
234
+ Renaming a column that’s in use will cause errors in your application.
235
+
236
+ ```ruby
237
+ class RenameSomeColumn < ActiveRecord::Migration[6.0]
238
+ def change
239
+ rename_column :users, :some_column, :new_name
240
+ end
241
+ end
242
+ ```
195
243
 
196
244
  #### Good
197
245
 
@@ -208,6 +256,8 @@ A safer approach is to:
208
256
 
209
257
  #### Bad
210
258
 
259
+ Renaming a table that’s in use will cause errors in your application.
260
+
211
261
  ```ruby
212
262
  class RenameUsersToCustomers < ActiveRecord::Migration[6.0]
213
263
  def change
@@ -259,33 +309,57 @@ end
259
309
 
260
310
  If you intend to drop an existing table, run `drop_table` first.
261
311
 
262
- ### Using change_column_null with a default value
312
+ ### Setting NOT NULL on an existing column
313
+
314
+ :turtle: Safe by default available
263
315
 
264
316
  #### Bad
265
317
 
266
- This generates a single `UPDATE` statement to set the default value.
318
+ Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
267
319
 
268
320
  ```ruby
269
- class ChangeSomeColumnNull < ActiveRecord::Migration[6.0]
321
+ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
270
322
  def change
271
- change_column_null :users, :some_column, false, "default_value"
323
+ change_column_null :users, :some_column, false
272
324
  end
273
325
  end
274
326
  ```
275
327
 
276
- #### Good
328
+ #### Good - Postgres
277
329
 
278
- Backfill the column [safely](#backfilling-data). Then use:
330
+ Instead, add a check constraint:
279
331
 
280
332
  ```ruby
281
- class ChangeSomeColumnNull < ActiveRecord::Migration[6.0]
333
+ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
282
334
  def change
335
+ safety_assured do
336
+ execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
337
+ end
338
+ end
339
+ end
340
+ ```
341
+
342
+ Then validate it in a separate migration. A `NOT NULL` check constraint is [functionally equivalent](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c) to setting `NOT NULL` on the column, but it won’t show up in `schema.rb`. In Postgres 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
343
+
344
+ ```ruby
345
+ class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
346
+ def change
347
+ safety_assured do
348
+ execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
349
+ end
350
+
351
+ # in Postgres 12+, you can then safely set NOT NULL on the column
283
352
  change_column_null :users, :some_column, false
353
+ safety_assured do
354
+ execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
355
+ end
284
356
  end
285
357
  end
286
358
  ```
287
359
 
288
- Note: In Postgres, `change_column_null` is still [not safe](#setting-not-null-on-an-existing-column) with this method.
360
+ #### Good - MySQL and MariaDB
361
+
362
+ [Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this.
289
363
 
290
364
  ### Executing SQL directly
291
365
 
@@ -301,9 +375,11 @@ end
301
375
 
302
376
  ### Adding an index non-concurrently
303
377
 
378
+ :turtle: Safe by default available
379
+
304
380
  #### Bad
305
381
 
306
- In Postgres, adding an index non-concurrently locks the table.
382
+ In Postgres, adding an index non-concurrently blocks writes.
307
383
 
308
384
  ```ruby
309
385
  class AddSomeIndexToUsers < ActiveRecord::Migration[6.0]
@@ -337,9 +413,11 @@ rails g index table column
337
413
 
338
414
  ### Adding a reference
339
415
 
416
+ :turtle: Safe by default available
417
+
340
418
  #### Bad
341
419
 
342
- Rails adds an index non-concurrently to references by default, which is problematic for Postgres.
420
+ Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
343
421
 
344
422
  ```ruby
345
423
  class AddReferenceToUsers < ActiveRecord::Migration[6.0]
@@ -365,9 +443,11 @@ end
365
443
 
366
444
  ### Adding a foreign key
367
445
 
446
+ :turtle: Safe by default available
447
+
368
448
  #### Bad
369
449
 
370
- 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).
450
+ In Postgres, adding a foreign key blocks writes on both tables.
371
451
 
372
452
  ```ruby
373
453
  class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0]
@@ -389,7 +469,7 @@ end
389
469
 
390
470
  #### Good
391
471
 
392
- 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).”
472
+ Add the foreign key without validating existing rows, then validate them in a separate migration.
393
473
 
394
474
  For Rails 5.2+, use:
395
475
 
@@ -401,7 +481,7 @@ class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0]
401
481
  end
402
482
  ```
403
483
 
404
- Then validate it in a separate migration.
484
+ Then:
405
485
 
406
486
  ```ruby
407
487
  class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.0]
@@ -423,7 +503,7 @@ class AddForeignKeyOnUsers < ActiveRecord::Migration[5.1]
423
503
  end
424
504
  ```
425
505
 
426
- Then validate it in a separate migration.
506
+ Then:
427
507
 
428
508
  ```ruby
429
509
  class ValidateForeignKeyOnUsers < ActiveRecord::Migration[5.1]
@@ -439,7 +519,7 @@ end
439
519
 
440
520
  #### Bad
441
521
 
442
- In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries.
522
+ In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
443
523
 
444
524
  ```ruby
445
525
  class AddPropertiesToUsers < ActiveRecord::Migration[6.0]
@@ -461,54 +541,6 @@ class AddPropertiesToUsers < ActiveRecord::Migration[6.0]
461
541
  end
462
542
  ```
463
543
 
464
- ### Setting NOT NULL on an existing column
465
-
466
- #### Bad
467
-
468
- In Postgres, setting `NOT NULL` on an existing column requires an `AccessExclusiveLock`, which is expensive on large tables.
469
-
470
- ```ruby
471
- class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
472
- def change
473
- change_column_null :users, :some_column, false
474
- end
475
- end
476
- ```
477
-
478
- #### Good
479
-
480
- Instead, add a constraint:
481
-
482
- ```ruby
483
- class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
484
- def change
485
- safety_assured do
486
- execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
487
- end
488
- end
489
- end
490
- ```
491
-
492
- Then validate it in a separate migration.
493
-
494
- ```ruby
495
- class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
496
- def change
497
- safety_assured do
498
- execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
499
- end
500
-
501
- # in Postgres 12+, you can safely turn this into a traditional column constraint
502
- change_column_null :users, :some_column, false
503
- safety_assured do
504
- execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
505
- end
506
- end
507
- end
508
- ```
509
-
510
- Note: This is not 100% the same as `NOT NULL` column constraint before Postgres 12. Here’s a [good explanation](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c).
511
-
512
544
  ### Keeping non-unique indexes to three columns or less
513
545
 
514
546
  #### Bad
@@ -551,6 +583,22 @@ end
551
583
 
552
584
  Certain methods like `execute` and `change_table` cannot be inspected and are prevented from running by default. Make sure what you’re doing is really safe and use this pattern.
553
585
 
586
+ ## Safe by Default
587
+
588
+ *Experimental*
589
+
590
+ Make operations safe by default.
591
+
592
+ - adding and removing an index
593
+ - adding a foreign key
594
+ - setting NOT NULL on an existing column
595
+
596
+ Add to `config/initializers/strong_migrations.rb`:
597
+
598
+ ```ruby
599
+ StrongMigrations.safe_by_default = true
600
+ ```
601
+
554
602
  ## Custom Checks
555
603
 
556
604
  Add your own custom checks with:
@@ -587,6 +635,14 @@ StrongMigrations.disable_check(:add_index)
587
635
 
588
636
  Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
589
637
 
638
+ ## Down Migrations / Rollbacks
639
+
640
+ By default, checks are disabled when migrating down. Enable them with:
641
+
642
+ ```ruby
643
+ StrongMigrations.check_down = true
644
+ ```
645
+
590
646
  ## Custom Messages
591
647
 
592
648
  To customize specific messages, create an initializer with:
@@ -597,7 +653,7 @@ StrongMigrations.error_messages[:add_column_default] = "Your custom instructions
597
653
 
598
654
  Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
599
655
 
600
- ## Timeouts
656
+ ## Migration Timeouts
601
657
 
602
658
  It’s extremely important to set a short lock timeout for migrations. This way, if a migration can’t acquire a lock in a timely manner, other statements won’t be stuck behind it. We also recommend setting a long statement timeout so migrations can run for a while.
603
659
 
@@ -617,6 +673,49 @@ ALTER ROLE myuser SET statement_timeout = '1h';
617
673
 
618
674
  Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
619
675
 
676
+ ## App Timeouts
677
+
678
+ We recommend adding timeouts to `config/database.yml` to prevent connections from hanging and individual queries from taking up too many resources in controllers, jobs, the Rails console, and other places.
679
+
680
+ For Postgres:
681
+
682
+ ```yml
683
+ production:
684
+ connect_timeout: 5
685
+ variables:
686
+ statement_timeout: 15s
687
+ lock_timeout: 10s
688
+ ```
689
+
690
+ Note: If you use PgBouncer in transaction mode, you must set the statement and lock timeouts on the database user as shown above.
691
+
692
+ For MySQL:
693
+
694
+ ```yml
695
+ production:
696
+ connect_timeout: 5
697
+ read_timeout: 5
698
+ write_timeout: 5
699
+ variables:
700
+ max_execution_time: 15000 # ms
701
+ lock_wait_timeout: 10 # sec
702
+
703
+ ```
704
+
705
+ For MariaDB:
706
+
707
+ ```yml
708
+ production:
709
+ connect_timeout: 5
710
+ read_timeout: 5
711
+ write_timeout: 5
712
+ variables:
713
+ max_statement_time: 15 # sec
714
+ lock_wait_timeout: 10 # sec
715
+ ```
716
+
717
+ For HTTP connections, Redis, and other services, check out [this guide](https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts).
718
+
620
719
  ## Existing Migrations
621
720
 
622
721
  To mark migrations as safe that were created before installing this gem, create an initializer with:
@@ -632,11 +731,11 @@ Use the version from your latest migration.
632
731
  If your development database version is different from production, you can specify the production version so the right checks run in development.
633
732
 
634
733
  ```ruby
635
- StrongMigrations.target_postgresql_version = "10"
636
- StrongMigrations.target_mysql_version = "8.0.12"
637
- StrongMigrations.target_mariadb_version = "10.3.2"
734
+ StrongMigrations.target_version = 10 # or "8.0.12", "10.3.2", etc
638
735
  ```
639
736
 
737
+ The major version works well for Postgres, while the full version is recommended for MySQL and MariaDB.
738
+
640
739
  For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
641
740
 
642
741
  ## Analyze Tables
@@ -664,22 +763,20 @@ Columns can flip order in `db/schema.rb` when you have multiple developers. One
664
763
  task "db:schema:dump": "strong_migrations:alphabetize_columns"
665
764
  ```
666
765
 
667
- ## Dangerous Tasks
668
-
669
- 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:
670
-
671
- ```sh
672
- SAFETY_ASSURED=1 rails db:drop
673
- ```
674
-
675
766
  ## Permissions
676
767
 
677
768
  We recommend using a [separate database user](https://ankane.org/postgres-users) for migrations when possible so you don’t need to grant your app user permission to alter tables.
678
769
 
770
+ ## Smaller Projects
771
+
772
+ You probably don’t need this gem for smaller projects, as operations that are unsafe at scale can be perfectly safe on smaller, low-traffic tables.
773
+
679
774
  ## Additional Reading
680
775
 
681
776
  - [Rails Migrations with No Downtime](https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
682
777
  - [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)
778
+ - [An Overview of DDL Algorithms in MySQL](https://mydbops.wordpress.com/2020/03/04/an-overview-of-ddl-algorithms-in-mysql-covers-mysql-8/)
779
+ - [MariaDB InnoDB Online DDL Overview](https://mariadb.com/kb/en/innodb-online-ddl-overview/)
683
780
 
684
781
  ## Credits
685
782
 
@@ -12,6 +12,31 @@ module StrongMigrations
12
12
  def start_after
13
13
  Time.now.utc.strftime("%Y%m%d%H%M%S")
14
14
  end
15
+
16
+ def pgbouncer_message
17
+ if postgresql?
18
+ "\n# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user"
19
+ end
20
+ end
21
+
22
+ def target_version
23
+ case adapter
24
+ when /mysql/
25
+ # could try to connect to database and check for MariaDB
26
+ # but this should be fine
27
+ '"8.0.12"'
28
+ else
29
+ "10"
30
+ end
31
+ end
32
+
33
+ def adapter
34
+ ActiveRecord::Base.connection_config[:adapter].to_s
35
+ end
36
+
37
+ def postgresql?
38
+ adapter =~ /postg/
39
+ end
15
40
  end
16
41
  end
17
42
  end
@@ -1,8 +1,7 @@
1
1
  # Mark existing migrations as safe
2
2
  StrongMigrations.start_after = <%= start_after %>
3
3
 
4
- # Set timeouts for migrations
5
- # If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user
4
+ # Set timeouts for migrations<%= pgbouncer_message %>
6
5
  StrongMigrations.lock_timeout = 10.seconds
7
6
  StrongMigrations.statement_timeout = 1.hour
8
7
 
@@ -10,9 +9,17 @@ StrongMigrations.statement_timeout = 1.hour
10
9
  # Outdated statistics can sometimes hurt performance
11
10
  StrongMigrations.auto_analyze = true
12
11
 
12
+ # Set the version of the production database
13
+ # so the right checks are run in development
14
+ # StrongMigrations.target_version = <%= target_version %>
15
+
13
16
  # Add custom checks
14
17
  # StrongMigrations.add_check do |method, args|
15
18
  # if method == :add_index && args[0].to_s == "users"
16
19
  # stop! "No more indexes on the users table"
17
20
  # end
18
- # end
21
+ # end<% if postgresql? %>
22
+
23
+ # Make some operations safe by default
24
+ # See https://github.com/ankane/strong_migrations#safe-by-default
25
+ # StrongMigrations.safe_by_default = true<% end %>
@@ -2,6 +2,7 @@
2
2
  require "active_support"
3
3
 
4
4
  # modules
5
+ require "strong_migrations/safe_methods"
5
6
  require "strong_migrations/checker"
6
7
  require "strong_migrations/database_tasks"
7
8
  require "strong_migrations/migration"
@@ -17,15 +18,17 @@ module StrongMigrations
17
18
  class << self
18
19
  attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
19
20
  :target_postgresql_version, :target_mysql_version, :target_mariadb_version,
20
- :enabled_checks, :lock_timeout, :statement_timeout
21
+ :enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version,
22
+ :safe_by_default
21
23
  attr_writer :lock_timeout_limit
22
24
  end
23
25
  self.auto_analyze = false
24
26
  self.start_after = 0
25
27
  self.checks = []
28
+ self.safe_by_default = false
26
29
  self.error_messages = {
27
30
  add_column_default:
28
- "Adding a column with a non-null default causes the entire table to be rewritten.
31
+ "Adding a column with a non-null default blocks %{rewrite_blocks} while the entire table is rewritten.
29
32
  Instead, add the column without a default value, then change the default.
30
33
 
31
34
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
@@ -50,12 +53,18 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
50
53
  end",
51
54
 
52
55
  add_column_json:
53
- "There's no equality operator for the json column type, which can
54
- cause errors for existing SELECT DISTINCT queries. Use jsonb instead.",
56
+ "There's no equality operator for the json column type, which can cause errors for
57
+ existing SELECT DISTINCT queries in your application. Use jsonb instead.
58
+
59
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
60
+ def change
61
+ %{command}
62
+ end
63
+ end",
55
64
 
56
65
  change_column:
57
- "Changing the type of an existing column requires the entire
58
- table and indexes to be rewritten. A safer approach is to:
66
+ "Changing the type of an existing column blocks %{rewrite_blocks}
67
+ while the entire table is rewritten. A safer approach is to:
59
68
 
60
69
  1. Create a new column
61
70
  2. Write to both columns
@@ -64,7 +73,10 @@ table and indexes to be rewritten. A safer approach is to:
64
73
  5. Stop writing to the old column
65
74
  6. Drop the old column",
66
75
 
67
- remove_column: "Active Record caches attributes which causes problems
76
+ change_column_with_not_null:
77
+ "Changing the type is safe, but setting NOT NULL is not.",
78
+
79
+ remove_column: "Active Record caches attributes, which causes problems
68
80
  when removing columns. Be sure to ignore the column%{column_suffix}:
69
81
 
70
82
  class %{model} < %{base_model}
@@ -80,7 +92,8 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
80
92
  end",
81
93
 
82
94
  rename_column:
83
- "Renaming a column is dangerous. A safer approach is to:
95
+ "Renaming a column that's in use will cause errors
96
+ in your application. A safer approach is to:
84
97
 
85
98
  1. Create a new column
86
99
  2. Write to both columns
@@ -90,7 +103,8 @@ end",
90
103
  6. Drop the old column",
91
104
 
92
105
  rename_table:
93
- "Renaming a table is dangerous. A safer approach is to:
106
+ "Renaming a table that's in use will cause errors
107
+ in your application. A safer approach is to:
94
108
 
95
109
  1. Create a new table. Don't forget to recreate indexes from the old table
96
110
  2. Write to both tables
@@ -111,7 +125,7 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
111
125
  end",
112
126
 
113
127
  add_index:
114
- "Adding an index non-concurrently locks the table. Instead, use:
128
+ "Adding an index non-concurrently blocks writes. Instead, use:
115
129
 
116
130
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
117
131
  disable_ddl_transaction!
@@ -122,7 +136,7 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
122
136
  end",
123
137
 
124
138
  remove_index:
125
- "Removing an index non-concurrently locks the table. Instead, use:
139
+ "Removing an index non-concurrently blocks writes. Instead, use:
126
140
 
127
141
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
128
142
  disable_ddl_transaction!
@@ -165,9 +179,8 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
165
179
  end",
166
180
 
167
181
  change_column_null_postgresql:
168
- "Setting NOT NULL on a column requires an AccessExclusiveLock,
169
- which is expensive on large tables. Instead, use a constraint and
170
- validate it in a separate migration with a more agreeable RowShareLock.
182
+ "Setting NOT NULL on an existing column blocks reads and writes while every row is checked.
183
+ Instead, add a check constraint and validate it in a separate migration.
171
184
 
172
185
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
173
186
  def change
@@ -185,9 +198,9 @@ end",
185
198
  "Setting NOT NULL on an existing column is not safe with your database engine.",
186
199
 
187
200
  add_foreign_key:
188
- "New foreign keys are validated by default. This acquires an AccessExclusiveLock,
189
- which is expensive on large tables. Instead, validate it in a separate migration
190
- with a more agreeable RowShareLock.
201
+ "Adding a foreign key blocks writes on both tables. Instead,
202
+ add the foreign key without validating existing rows,
203
+ then validate them in a separate migration.
191
204
 
192
205
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
193
206
  def change
@@ -199,9 +212,14 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
199
212
  def change
200
213
  %{validate_foreign_key_code}
201
214
  end
202
- end"
215
+ end",
216
+
217
+ validate_foreign_key:
218
+ "Validating a foreign key while writes are blocked is dangerous.
219
+ Use disable_ddl_transaction! or a separate migration."
203
220
  }
204
221
  self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
222
+ self.check_down = false
205
223
 
206
224
  # private
207
225
  def self.developer_env?
@@ -1,6 +1,8 @@
1
1
  module StrongMigrations
2
2
  class Checker
3
- attr_accessor :direction
3
+ include SafeMethods
4
+
5
+ attr_accessor :direction, :transaction_disabled
4
6
 
5
7
  def initialize(migration)
6
8
  @migration = migration
@@ -24,7 +26,7 @@ module StrongMigrations
24
26
  set_timeouts
25
27
  check_lock_timeout
26
28
 
27
- unless safe?
29
+ if !safe? || safe_by_default_method?(method)
28
30
  case method
29
31
  when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
30
32
  columns =
@@ -65,6 +67,7 @@ module StrongMigrations
65
67
  raise_error :add_index_columns, header: "Best practice"
66
68
  end
67
69
  if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
70
+ return safe_add_index(table, columns, options) if StrongMigrations.safe_by_default
68
71
  raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
69
72
  end
70
73
  when :remove_index
@@ -75,6 +78,7 @@ module StrongMigrations
75
78
  options ||= {}
76
79
 
77
80
  if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
81
+ return safe_remove_index(table, options) if StrongMigrations.safe_by_default
78
82
  raise_error :remove_index, command: command_str("remove_index", [table, options.merge(algorithm: :concurrently)])
79
83
  end
80
84
  when :add_column
@@ -96,11 +100,13 @@ Then add the NOT NULL constraint in separate migrations."
96
100
  change_command: command_str("change_column_default", [table, column, default]),
97
101
  remove_command: command_str("remove_column", [table, column]),
98
102
  code: backfill_code(table, column, default),
99
- append: append
103
+ append: append,
104
+ rewrite_blocks: rewrite_blocks
100
105
  end
101
106
 
102
107
  if type.to_s == "json" && postgresql?
103
- raise_error :add_column_json
108
+ raise_error :add_column_json,
109
+ command: command_str("add_column", [table, column, :jsonb, options])
104
110
  end
105
111
  when :change_column
106
112
  table, column, type, options = args
@@ -109,15 +115,24 @@ Then add the NOT NULL constraint in separate migrations."
109
115
  safe = false
110
116
  existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
111
117
  if existing_column
112
- sql_type = existing_column.sql_type.split("(").first
118
+ existing_type = existing_column.sql_type.split("(").first
113
119
  if postgresql?
114
120
  case type.to_s
115
- when "string", "text"
116
- # safe to change limit for varchar
117
- safe = ["character varying", "text"].include?(sql_type)
121
+ when "string"
122
+ # safe to increase limit or remove it
123
+ # not safe to decrease limit or add a limit
124
+ case existing_type
125
+ when "character varying"
126
+ safe = !options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
127
+ when "text"
128
+ safe = !options[:limit]
129
+ end
130
+ when "text"
131
+ # safe to change varchar to text (and text to text)
132
+ safe = ["character varying", "text"].include?(existing_type)
118
133
  when "numeric", "decimal"
119
134
  # numeric and decimal are equivalent and can be used interchangably
120
- safe = ["numeric", "decimal"].include?(sql_type) &&
135
+ safe = ["numeric", "decimal"].include?(existing_type) &&
121
136
  (
122
137
  (
123
138
  # unconstrained
@@ -130,7 +145,7 @@ Then add the NOT NULL constraint in separate migrations."
130
145
  )
131
146
  )
132
147
  when "datetime", "timestamp", "timestamptz"
133
- safe = ["timestamp without time zone", "timestamp with time zone"].include?(sql_type) &&
148
+ safe = ["timestamp without time zone", "timestamp with time zone"].include?(existing_type) &&
134
149
  postgresql_version >= Gem::Version.new("12") &&
135
150
  connection.select_all("SHOW timezone").first["TimeZone"] == "UTC"
136
151
  end
@@ -142,13 +157,19 @@ Then add the NOT NULL constraint in separate migrations."
142
157
  # increased limit, but doesn't change number of length bytes
143
158
  # 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
144
159
  limit = options[:limit] || 255
145
- safe = ["varchar"].include?(sql_type) &&
160
+ safe = ["varchar"].include?(existing_type) &&
146
161
  limit >= existing_column.limit &&
147
162
  (limit <= 255 || existing_column.limit > 255)
148
163
  end
149
164
  end
150
165
  end
151
- raise_error :change_column unless safe
166
+
167
+ # unsafe to set NOT NULL for safe types
168
+ if safe && existing_column.null && options[:null] == false
169
+ raise_error :change_column_with_not_null
170
+ end
171
+
172
+ raise_error :change_column, rewrite_blocks: rewrite_blocks unless safe
152
173
  when :create_table
153
174
  table, options = args
154
175
  options ||= {}
@@ -167,16 +188,16 @@ Then add the NOT NULL constraint in separate migrations."
167
188
  bad_index = index_value && !concurrently_set
168
189
 
169
190
  if bad_index || options[:foreign_key]
170
- columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
171
-
172
191
  if index_value.is_a?(Hash)
173
192
  options[:index] = options[:index].merge(algorithm: :concurrently)
174
193
  else
175
194
  options = options.merge(index: {algorithm: :concurrently})
176
195
  end
177
196
 
197
+ return safe_add_reference(table, reference, options) if StrongMigrations.safe_by_default
198
+
178
199
  if options.delete(:foreign_key)
179
- headline = "Adding a validated foreign key locks the table."
200
+ headline = "Adding a foreign key blocks writes on both tables."
180
201
  append = "
181
202
 
182
203
  Then add the foreign key in separate migrations."
@@ -206,14 +227,22 @@ Then add the foreign key in separate migrations."
206
227
  # match https://github.com/nullobject/rein
207
228
  constraint_name = "#{table}_#{column}_null"
208
229
 
209
- validate_constraint_code = String.new(constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name]))
230
+ add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
231
+ validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
232
+ remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
233
+
234
+ validate_constraint_code = String.new(safety_assured_str(validate_code))
210
235
  if postgresql_version >= Gem::Version.new("12")
211
- validate_constraint_code << "\n #{command_str(:change_column_null, [table, column, null])}"
212
- validate_constraint_code << "\n #{constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])}"
236
+ change_args = [table, column, null]
237
+
238
+ validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
239
+ validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
213
240
  end
214
241
 
242
+ return safe_change_column_null(add_code, validate_code, change_args, remove_code) if StrongMigrations.safe_by_default
243
+
215
244
  raise_error :change_column_null_postgresql,
216
- add_constraint_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
245
+ add_constraint_code: safety_assured_str(add_code),
217
246
  validate_constraint_code: validate_constraint_code
218
247
  end
219
248
  elsif mysql? || mariadb?
@@ -238,15 +267,26 @@ Then add the foreign key in separate migrations."
238
267
  hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10)
239
268
  fk_name = options[:name] || "fk_rails_#{hashed_identifier}"
240
269
 
270
+ add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key])
271
+ validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
272
+
273
+ return safe_add_foreign_key_code(from_table, to_table, add_code, validate_code) if StrongMigrations.safe_by_default
274
+
241
275
  raise_error :add_foreign_key,
242
- add_foreign_key_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key]),
243
- validate_foreign_key_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
276
+ add_foreign_key_code: safety_assured_str(add_code),
277
+ validate_foreign_key_code: safety_assured_str(validate_code)
244
278
  else
279
+ return safe_add_foreign_key(from_table, to_table, options) if StrongMigrations.safe_by_default
280
+
245
281
  raise_error :add_foreign_key,
246
282
  add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
247
283
  validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
248
284
  end
249
285
  end
286
+ when :validate_foreign_key
287
+ if postgresql? && writes_blocked?
288
+ raise_error :validate_foreign_key
289
+ end
250
290
  end
251
291
 
252
292
  StrongMigrations.checks.each do |check|
@@ -259,8 +299,7 @@ Then add the foreign key in separate migrations."
259
299
  # outdated statistics + a new index can hurt performance of existing queries
260
300
  if StrongMigrations.auto_analyze && direction == :up && method == :add_index
261
301
  if postgresql?
262
- # TODO remove verbose in 0.7.0
263
- connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}"
302
+ connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}"
264
303
  elsif mariadb? || mysql?
265
304
  connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
266
305
  end
@@ -269,6 +308,8 @@ Then add the foreign key in separate migrations."
269
308
  result
270
309
  end
271
310
 
311
+ private
312
+
272
313
  def set_timeouts
273
314
  if !@timeouts_set
274
315
  if StrongMigrations.statement_timeout
@@ -276,7 +317,8 @@ Then add the foreign key in separate migrations."
276
317
  if postgresql?
277
318
  "SET statement_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.statement_timeout))}"
278
319
  elsif mysql?
279
- "SET max_execution_time = #{connection.quote(StrongMigrations.statement_timeout.to_i * 1000)}"
320
+ # use ceil to prevent no timeout for values under 1 ms
321
+ "SET max_execution_time = #{connection.quote((StrongMigrations.statement_timeout.to_f * 1000).ceil)}"
280
322
  elsif mariadb?
281
323
  "SET max_statement_time = #{connection.quote(StrongMigrations.statement_timeout)}"
282
324
  else
@@ -303,8 +345,6 @@ Then add the foreign key in separate migrations."
303
345
  end
304
346
  end
305
347
 
306
- private
307
-
308
348
  def connection
309
349
  @migration.connection
310
350
  end
@@ -314,7 +354,8 @@ Then add the foreign key in separate migrations."
314
354
  end
315
355
 
316
356
  def safe?
317
- @safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) || direction == :down || version_safe?
357
+ @safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) ||
358
+ (direction == :down && !StrongMigrations.check_down) || version_safe?
318
359
  end
319
360
 
320
361
  def version_safe?
@@ -359,6 +400,7 @@ Then add the foreign key in separate migrations."
359
400
  end
360
401
 
361
402
  def target_version(target_version)
403
+ target_version ||= StrongMigrations.target_version
362
404
  version =
363
405
  if target_version && StrongMigrations.developer_env?
364
406
  target_version.to_s
@@ -382,6 +424,7 @@ Then add the foreign key in separate migrations."
382
424
  end
383
425
  elsif mysql? || mariadb?
384
426
  lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
427
+ # lock timeout is an integer
385
428
  if lock_timeout.to_i > limit
386
429
  warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
387
430
  end
@@ -414,16 +457,22 @@ Then add the foreign key in separate migrations."
414
457
  if timeout.is_a?(String)
415
458
  timeout
416
459
  else
417
- timeout.to_i * 1000
460
+ # use ceil to prevent no timeout for values under 1 ms
461
+ (timeout.to_f * 1000).ceil
418
462
  end
419
463
  end
420
464
 
421
465
  def constraints(table_name)
422
- query = <<-SQL
423
- SELECT conname AS name, pg_get_constraintdef(oid) AS def FROM pg_constraint
424
- WHERE contype = 'c'
425
- AND convalidated
426
- AND conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
466
+ query = <<~SQL
467
+ SELECT
468
+ conname AS name,
469
+ pg_get_constraintdef(oid) AS def
470
+ FROM
471
+ pg_constraint
472
+ WHERE
473
+ contype = 'c' AND
474
+ convalidated AND
475
+ conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
427
476
  SQL
428
477
  connection.select_all(query.squish).to_a
429
478
  end
@@ -445,7 +494,10 @@ Then add the foreign key in separate migrations."
445
494
 
446
495
  def constraint_str(statement, identifiers)
447
496
  # not all identifiers are tables, but this method of quoting should be fine
448
- code = statement % identifiers.map { |v| connection.quote_table_name(v) }
497
+ statement % identifiers.map { |v| connection.quote_table_name(v) }
498
+ end
499
+
500
+ def safety_assured_str(code)
449
501
  "safety_assured do\n execute '#{code}' \n end"
450
502
  end
451
503
 
@@ -472,6 +524,23 @@ Then add the foreign key in separate migrations."
472
524
  "#{command} #{str_args.join(", ")}"
473
525
  end
474
526
 
527
+ def writes_blocked?
528
+ query = <<~SQL
529
+ SELECT
530
+ relation::regclass::text
531
+ FROM
532
+ pg_locks
533
+ WHERE
534
+ mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
535
+ pid = pg_backend_pid()
536
+ SQL
537
+ connection.select_all(query.squish).any?
538
+ end
539
+
540
+ def rewrite_blocks
541
+ mysql? || mariadb? ? "writes" : "reads and writes"
542
+ end
543
+
475
544
  def backfill_code(table, column, default)
476
545
  model = table.to_s.classify
477
546
  "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
@@ -3,6 +3,7 @@ module StrongMigrations
3
3
  def migrate(direction)
4
4
  strong_migrations_checker.direction = direction
5
5
  super
6
+ connection.begin_db_transaction if strong_migrations_checker.transaction_disabled
6
7
  end
7
8
 
8
9
  def method_missing(method, *args)
@@ -5,10 +5,6 @@ module StrongMigrations
5
5
  class Railtie < Rails::Railtie
6
6
  rake_tasks do
7
7
  load "tasks/strong_migrations.rake"
8
-
9
- ["db:drop", "db:reset", "db:schema:load", "db:structure:load"].each do |t|
10
- Rake::Task[t].enhance ["strong_migrations:safety_assured"]
11
- end
12
8
  end
13
9
  end
14
10
  end
@@ -0,0 +1,112 @@
1
+ module StrongMigrations
2
+ module SafeMethods
3
+ def safe_by_default_method?(method)
4
+ StrongMigrations.safe_by_default && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :change_column_null].include?(method)
5
+ end
6
+
7
+ # TODO check if invalid index with expected name exists and remove if needed
8
+ def safe_add_index(table, columns, options)
9
+ disable_transaction
10
+ @migration.add_index(table, columns, **options.merge(algorithm: :concurrently))
11
+ end
12
+
13
+ def safe_remove_index(table, options)
14
+ disable_transaction
15
+ @migration.remove_index(table, **options.merge(algorithm: :concurrently))
16
+ end
17
+
18
+ def safe_add_reference(table, reference, options)
19
+ @migration.reversible do |dir|
20
+ dir.up do
21
+ disable_transaction
22
+ foreign_key = options.delete(:foreign_key)
23
+ @migration.add_reference(table, reference, **options)
24
+ if foreign_key
25
+ # same as Active Record
26
+ name =
27
+ if foreign_key.is_a?(Hash) && foreign_key[:to_table]
28
+ foreign_key[:to_table]
29
+ else
30
+ (ActiveRecord::Base.pluralize_table_names ? reference.to_s.pluralize : reference).to_sym
31
+ end
32
+
33
+ @migration.add_foreign_key(table, name)
34
+ end
35
+ end
36
+ dir.down do
37
+ @migration.remove_reference(table, reference)
38
+ end
39
+ end
40
+ end
41
+
42
+ def safe_add_foreign_key(from_table, to_table, options)
43
+ @migration.reversible do |dir|
44
+ dir.up do
45
+ @migration.add_foreign_key(from_table, to_table, **options.merge(validate: false))
46
+ disable_transaction
47
+ @migration.validate_foreign_key(from_table, to_table)
48
+ end
49
+ dir.down do
50
+ @migration.remove_foreign_key(from_table, to_table)
51
+ end
52
+ end
53
+ end
54
+
55
+ def safe_add_foreign_key_code(from_table, to_table, add_code, validate_code)
56
+ @migration.reversible do |dir|
57
+ dir.up do
58
+ @migration.safety_assured do
59
+ @migration.execute(add_code)
60
+ disable_transaction
61
+ @migration.execute(validate_code)
62
+ end
63
+ end
64
+ dir.down do
65
+ @migration.remove_foreign_key(from_table, to_table)
66
+ end
67
+ end
68
+ end
69
+
70
+ def safe_change_column_null(add_code, validate_code, change_args, remove_code)
71
+ @migration.reversible do |dir|
72
+ dir.up do
73
+ @migration.safety_assured do
74
+ @migration.execute(add_code)
75
+ disable_transaction
76
+ @migration.execute(validate_code)
77
+ end
78
+ if change_args
79
+ @migration.change_column_null(*change_args)
80
+ @migration.safety_assured do
81
+ @migration.execute(remove_code)
82
+ end
83
+ end
84
+ end
85
+ dir.down do
86
+ if change_args
87
+ down_args = change_args.dup
88
+ down_args[2] = true
89
+ @migration.change_column_null(*down_args)
90
+ else
91
+ @migration.safety_assured do
92
+ @migration.execute(remove_code)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ # hard to commit at right time when reverting
100
+ # so just commit at start
101
+ def disable_transaction
102
+ if in_transaction? && !transaction_disabled
103
+ @migration.connection.commit_db_transaction
104
+ self.transaction_disabled = true
105
+ end
106
+ end
107
+
108
+ def in_transaction?
109
+ @migration.connection.open_transactions > 0
110
+ end
111
+ end
112
+ end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.6.8"
2
+ VERSION = "0.7.4"
3
3
  end
@@ -1,10 +1,4 @@
1
- # https://nithinbekal.com/posts/safe-rake-tasks
2
-
3
1
  namespace :strong_migrations do
4
- task safety_assured: :environment do
5
- raise "Set SAFETY_ASSURED=1 to run this task in production" if Rails.env.production? && !ENV["SAFETY_ASSURED"]
6
- end
7
-
8
2
  # https://www.pgrs.net/2008/03/13/alphabetize-schema-rb-columns/
9
3
  task :alphabetize_columns do
10
4
  $stderr.puts "Dumping schema"
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strong_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.8
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  - Bob Remeika
9
9
  - David Waller
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-05-14 00:00:00.000000000 Z
13
+ date: 2020-12-16 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -96,7 +96,7 @@ dependencies:
96
96
  - - ">="
97
97
  - !ruby/object:Gem::Version
98
98
  version: '0'
99
- description:
99
+ description:
100
100
  email:
101
101
  - andrew@chartkick.com
102
102
  - bob.remeika@gmail.com
@@ -116,13 +116,14 @@ files:
116
116
  - lib/strong_migrations/database_tasks.rb
117
117
  - lib/strong_migrations/migration.rb
118
118
  - lib/strong_migrations/railtie.rb
119
+ - lib/strong_migrations/safe_methods.rb
119
120
  - lib/strong_migrations/version.rb
120
121
  - lib/tasks/strong_migrations.rake
121
122
  homepage: https://github.com/ankane/strong_migrations
122
123
  licenses:
123
124
  - MIT
124
125
  metadata: {}
125
- post_install_message:
126
+ post_install_message:
126
127
  rdoc_options: []
127
128
  require_paths:
128
129
  - lib
@@ -137,8 +138,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
138
  - !ruby/object:Gem::Version
138
139
  version: '0'
139
140
  requirements: []
140
- rubygems_version: 3.1.2
141
- signing_key:
141
+ rubygems_version: 3.2.0.rc.1
142
+ signing_key:
142
143
  specification_version: 4
143
144
  summary: Catch unsafe migrations in development
144
145
  test_files: []