strong_migrations 0.6.8 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f64d60856479de1a9071b64be2186256e619c30a6caea46d2f8094fb1d3ded4b
4
- data.tar.gz: 586c15c10b53ba6273685d3d4bbeaa84f0c23fc973643abac1a2687d3ad2e986
3
+ metadata.gz: 2ef314694846eec868e7e01b74fcfc9a29d361e01e3bc9102914a0bd3ba0facc
4
+ data.tar.gz: b27a43c73b0c394bd30579db15a412ade64d53fcc72a5d9a9b8fc35695d3aedd
5
5
  SHA512:
6
- metadata.gz: d5aac390d0d1f965819311ec4ad0cc18b66eb310768f5ed95635670ca646f8804cf0fb672c94f4f1b59f6d812b027cc2d666607bf9b55d5a5f58f37726056d8c
7
- data.tar.gz: f0ec6e96d589aa99df461bbcc29201142a25a74a951edbbdf72c49e8a1d81a58623ccb6f049d2630a5cbd517f9edf9119511087d51da90f95c9794a13f5c1fb1
6
+ metadata.gz: 793918328d63451ef6a6ec3b90b3d2a4082ef2c57e018fa0f84e223defe506c3722f7e2168ef97854fcade562dc115943d9655954fdd9a319a13dca6ec50648b
7
+ data.tar.gz: c353256d7854ea1240bfc1e3f6e0ce7b8c76b72404f728c37695f923c4189777463481d57abc55efdce7a0326621a6a61ca9fcb98638e2f2aa279496d126bd20
@@ -1,3 +1,13 @@
1
+ ## 0.7.0 (2020-07-22)
2
+
3
+ - Added `check_down` option
4
+ - Added check for `change_column` with `null: false`
5
+ - Added check for `validate_foreign_key`
6
+ - Improved error messages
7
+ - Made auto analyze less verbose in Postgres
8
+ - Decreasing the length limit of a `varchar` column or adding a limit is not safe in Postgres
9
+ - Removed safety checks for `db` rake tasks (Rails 5+ handles this)
10
+
1
11
  ## 0.6.8 (2020-05-13)
2
12
 
3
13
  - `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
@@ -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, :string }
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,55 @@ 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
263
313
 
264
314
  #### Bad
265
315
 
266
- This generates a single `UPDATE` statement to set the default value.
316
+ Setting `NOT NULL` on an existing column blocks reads and writes while the every row is checked.
267
317
 
268
318
  ```ruby
269
- class ChangeSomeColumnNull < ActiveRecord::Migration[6.0]
319
+ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
270
320
  def change
271
- change_column_null :users, :some_column, false, "default_value"
321
+ change_column_null :users, :some_column, false
272
322
  end
273
323
  end
274
324
  ```
275
325
 
276
- #### Good
326
+ #### Good - Postgres
327
+
328
+ Instead, add a check constraint:
329
+
330
+ ```ruby
331
+ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
332
+ def change
333
+ safety_assured do
334
+ execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
335
+ end
336
+ end
337
+ end
338
+ ```
277
339
 
278
- Backfill the column [safely](#backfilling-data). Then use:
340
+ 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.
279
341
 
280
342
  ```ruby
281
- class ChangeSomeColumnNull < ActiveRecord::Migration[6.0]
343
+ class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
282
344
  def change
345
+ safety_assured do
346
+ execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
347
+ end
348
+
349
+ # in Postgres 12+, you can then safely set NOT NULL on the column
283
350
  change_column_null :users, :some_column, false
351
+ safety_assured do
352
+ execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
353
+ end
284
354
  end
285
355
  end
286
356
  ```
287
357
 
288
- Note: In Postgres, `change_column_null` is still [not safe](#setting-not-null-on-an-existing-column) with this method.
358
+ #### Good - MySQL and MariaDB
359
+
360
+ [Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this.
289
361
 
290
362
  ### Executing SQL directly
291
363
 
@@ -303,7 +375,7 @@ end
303
375
 
304
376
  #### Bad
305
377
 
306
- In Postgres, adding an index non-concurrently locks the table.
378
+ In Postgres, adding an index non-concurrently blocks writes.
307
379
 
308
380
  ```ruby
309
381
  class AddSomeIndexToUsers < ActiveRecord::Migration[6.0]
@@ -339,7 +411,7 @@ rails g index table column
339
411
 
340
412
  #### Bad
341
413
 
342
- Rails adds an index non-concurrently to references by default, which is problematic for Postgres.
414
+ Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
343
415
 
344
416
  ```ruby
345
417
  class AddReferenceToUsers < ActiveRecord::Migration[6.0]
@@ -367,7 +439,7 @@ end
367
439
 
368
440
  #### Bad
369
441
 
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).
442
+ In Postgres, adding a foreign key blocks writes on both tables.
371
443
 
372
444
  ```ruby
373
445
  class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0]
@@ -389,7 +461,7 @@ end
389
461
 
390
462
  #### Good
391
463
 
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).”
464
+ Add the foreign key without validating existing rows, then validate them in a separate migration.
393
465
 
394
466
  For Rails 5.2+, use:
395
467
 
@@ -401,7 +473,7 @@ class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0]
401
473
  end
402
474
  ```
403
475
 
404
- Then validate it in a separate migration.
476
+ Then:
405
477
 
406
478
  ```ruby
407
479
  class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.0]
@@ -423,7 +495,7 @@ class AddForeignKeyOnUsers < ActiveRecord::Migration[5.1]
423
495
  end
424
496
  ```
425
497
 
426
- Then validate it in a separate migration.
498
+ Then:
427
499
 
428
500
  ```ruby
429
501
  class ValidateForeignKeyOnUsers < ActiveRecord::Migration[5.1]
@@ -439,7 +511,7 @@ end
439
511
 
440
512
  #### Bad
441
513
 
442
- In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries.
514
+ 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
515
 
444
516
  ```ruby
445
517
  class AddPropertiesToUsers < ActiveRecord::Migration[6.0]
@@ -461,54 +533,6 @@ class AddPropertiesToUsers < ActiveRecord::Migration[6.0]
461
533
  end
462
534
  ```
463
535
 
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
536
  ### Keeping non-unique indexes to three columns or less
513
537
 
514
538
  #### Bad
@@ -587,6 +611,14 @@ StrongMigrations.disable_check(:add_index)
587
611
 
588
612
  Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
589
613
 
614
+ ## Down Migrations / Rollbacks [unreleased]
615
+
616
+ By default, checks are disabled when migrating down. Enable them with:
617
+
618
+ ```ruby
619
+ StrongMigrations.check_down = true
620
+ ```
621
+
590
622
  ## Custom Messages
591
623
 
592
624
  To customize specific messages, create an initializer with:
@@ -597,7 +629,7 @@ StrongMigrations.error_messages[:add_column_default] = "Your custom instructions
597
629
 
598
630
  Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
599
631
 
600
- ## Timeouts
632
+ ## Migration Timeouts
601
633
 
602
634
  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
635
 
@@ -617,6 +649,49 @@ ALTER ROLE myuser SET statement_timeout = '1h';
617
649
 
618
650
  Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
619
651
 
652
+ ## App Timeouts
653
+
654
+ 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.
655
+
656
+ For Postgres:
657
+
658
+ ```yml
659
+ production:
660
+ connect_timeout: 5
661
+ variables:
662
+ statement_timeout: 15s
663
+ lock_timeout: 10s
664
+ ```
665
+
666
+ Note: If you use PgBouncer in transaction mode, you must set the statement and lock timeouts on the database user as shown above.
667
+
668
+ For MySQL:
669
+
670
+ ```yml
671
+ production:
672
+ connect_timeout: 5
673
+ read_timeout: 5
674
+ write_timeout: 5
675
+ variables:
676
+ max_execution_time: 15000 # ms
677
+ lock_wait_timeout: 10 # sec
678
+
679
+ ```
680
+
681
+ For MariaDB:
682
+
683
+ ```yml
684
+ production:
685
+ connect_timeout: 5
686
+ read_timeout: 5
687
+ write_timeout: 5
688
+ variables:
689
+ max_statement_time: 15 # sec
690
+ lock_wait_timeout: 10 # sec
691
+ ```
692
+
693
+ For HTTP connections, Redis, and other services, check out [this guide](https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts).
694
+
620
695
  ## Existing Migrations
621
696
 
622
697
  To mark migrations as safe that were created before installing this gem, create an initializer with:
@@ -664,22 +739,20 @@ Columns can flip order in `db/schema.rb` when you have multiple developers. One
664
739
  task "db:schema:dump": "strong_migrations:alphabetize_columns"
665
740
  ```
666
741
 
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
742
  ## Permissions
676
743
 
677
744
  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
745
 
746
+ ## Smaller Projects
747
+
748
+ 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.
749
+
679
750
  ## Additional Reading
680
751
 
681
752
  - [Rails Migrations with No Downtime](https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
682
753
  - [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)
754
+ - [An Overview of DDL Algorithms in MySQL](https://mydbops.wordpress.com/2020/03/04/an-overview-of-ddl-algorithms-in-mysql-covers-mysql-8/)
755
+ - [MariaDB InnoDB Online DDL Overview](https://mariadb.com/kb/en/innodb-online-ddl-overview/)
683
756
 
684
757
  ## Credits
685
758
 
@@ -17,7 +17,7 @@ module StrongMigrations
17
17
  class << self
18
18
  attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
19
19
  :target_postgresql_version, :target_mysql_version, :target_mariadb_version,
20
- :enabled_checks, :lock_timeout, :statement_timeout
20
+ :enabled_checks, :lock_timeout, :statement_timeout, :check_down
21
21
  attr_writer :lock_timeout_limit
22
22
  end
23
23
  self.auto_analyze = false
@@ -25,7 +25,7 @@ module StrongMigrations
25
25
  self.checks = []
26
26
  self.error_messages = {
27
27
  add_column_default:
28
- "Adding a column with a non-null default causes the entire table to be rewritten.
28
+ "Adding a column with a non-null default blocks %{rewrite_blocks} while the entire table is rewritten.
29
29
  Instead, add the column without a default value, then change the default.
30
30
 
31
31
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
@@ -50,12 +50,18 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
50
50
  end",
51
51
 
52
52
  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.",
53
+ "There's no equality operator for the json column type, which can cause errors for
54
+ existing SELECT DISTINCT queries in your application. Use jsonb instead.
55
+
56
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
57
+ def change
58
+ %{command}
59
+ end
60
+ end",
55
61
 
56
62
  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:
63
+ "Changing the type of an existing column blocks %{rewrite_blocks}
64
+ while the entire table is rewritten. A safer approach is to:
59
65
 
60
66
  1. Create a new column
61
67
  2. Write to both columns
@@ -64,7 +70,10 @@ table and indexes to be rewritten. A safer approach is to:
64
70
  5. Stop writing to the old column
65
71
  6. Drop the old column",
66
72
 
67
- remove_column: "Active Record caches attributes which causes problems
73
+ change_column_with_not_null:
74
+ "Changing the type is safe, but setting NOT NULL is not.",
75
+
76
+ remove_column: "Active Record caches attributes, which causes problems
68
77
  when removing columns. Be sure to ignore the column%{column_suffix}:
69
78
 
70
79
  class %{model} < %{base_model}
@@ -80,7 +89,8 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
80
89
  end",
81
90
 
82
91
  rename_column:
83
- "Renaming a column is dangerous. A safer approach is to:
92
+ "Renaming a column that's in use will cause errors
93
+ in your application. A safer approach is to:
84
94
 
85
95
  1. Create a new column
86
96
  2. Write to both columns
@@ -90,7 +100,8 @@ end",
90
100
  6. Drop the old column",
91
101
 
92
102
  rename_table:
93
- "Renaming a table is dangerous. A safer approach is to:
103
+ "Renaming a table that's in use will cause errors
104
+ in your application. A safer approach is to:
94
105
 
95
106
  1. Create a new table. Don't forget to recreate indexes from the old table
96
107
  2. Write to both tables
@@ -111,7 +122,7 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
111
122
  end",
112
123
 
113
124
  add_index:
114
- "Adding an index non-concurrently locks the table. Instead, use:
125
+ "Adding an index non-concurrently blocks writes. Instead, use:
115
126
 
116
127
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
117
128
  disable_ddl_transaction!
@@ -122,7 +133,7 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
122
133
  end",
123
134
 
124
135
  remove_index:
125
- "Removing an index non-concurrently locks the table. Instead, use:
136
+ "Removing an index non-concurrently blocks writes. Instead, use:
126
137
 
127
138
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
128
139
  disable_ddl_transaction!
@@ -165,9 +176,8 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
165
176
  end",
166
177
 
167
178
  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.
179
+ "Setting NOT NULL on an existing column blocks reads and writes while every row is checked.
180
+ Instead, add a check constraint and validate it in a separate migration.
171
181
 
172
182
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
173
183
  def change
@@ -185,9 +195,9 @@ end",
185
195
  "Setting NOT NULL on an existing column is not safe with your database engine.",
186
196
 
187
197
  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.
198
+ "Adding a foreign key blocks writes on both tables. Instead,
199
+ add the foreign key without validating existing rows,
200
+ then validate them in a separate migration.
191
201
 
192
202
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
193
203
  def change
@@ -199,9 +209,14 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
199
209
  def change
200
210
  %{validate_foreign_key_code}
201
211
  end
202
- end"
212
+ end",
213
+
214
+ validate_foreign_key:
215
+ "Validating a foreign key while writes are blocked is dangerous.
216
+ Use disable_ddl_transaction! or a separate migration."
203
217
  }
204
218
  self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
219
+ self.check_down = false
205
220
 
206
221
  # private
207
222
  def self.developer_env?
@@ -96,11 +96,13 @@ Then add the NOT NULL constraint in separate migrations."
96
96
  change_command: command_str("change_column_default", [table, column, default]),
97
97
  remove_command: command_str("remove_column", [table, column]),
98
98
  code: backfill_code(table, column, default),
99
- append: append
99
+ append: append,
100
+ rewrite_blocks: rewrite_blocks
100
101
  end
101
102
 
102
103
  if type.to_s == "json" && postgresql?
103
- raise_error :add_column_json
104
+ raise_error :add_column_json,
105
+ command: command_str("add_column", [table, column, :jsonb, options])
104
106
  end
105
107
  when :change_column
106
108
  table, column, type, options = args
@@ -109,15 +111,24 @@ Then add the NOT NULL constraint in separate migrations."
109
111
  safe = false
110
112
  existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
111
113
  if existing_column
112
- sql_type = existing_column.sql_type.split("(").first
114
+ existing_type = existing_column.sql_type.split("(").first
113
115
  if postgresql?
114
116
  case type.to_s
115
- when "string", "text"
116
- # safe to change limit for varchar
117
- safe = ["character varying", "text"].include?(sql_type)
117
+ when "string"
118
+ # safe to increase limit or remove it
119
+ # not safe to decrease limit or add a limit
120
+ case existing_type
121
+ when "character varying"
122
+ safe = !options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
123
+ when "text"
124
+ safe = !options[:limit]
125
+ end
126
+ when "text"
127
+ # safe to change varchar to text (and text to text)
128
+ safe = ["character varying", "text"].include?(existing_type)
118
129
  when "numeric", "decimal"
119
130
  # numeric and decimal are equivalent and can be used interchangably
120
- safe = ["numeric", "decimal"].include?(sql_type) &&
131
+ safe = ["numeric", "decimal"].include?(existing_type) &&
121
132
  (
122
133
  (
123
134
  # unconstrained
@@ -130,7 +141,7 @@ Then add the NOT NULL constraint in separate migrations."
130
141
  )
131
142
  )
132
143
  when "datetime", "timestamp", "timestamptz"
133
- safe = ["timestamp without time zone", "timestamp with time zone"].include?(sql_type) &&
144
+ safe = ["timestamp without time zone", "timestamp with time zone"].include?(existing_type) &&
134
145
  postgresql_version >= Gem::Version.new("12") &&
135
146
  connection.select_all("SHOW timezone").first["TimeZone"] == "UTC"
136
147
  end
@@ -142,13 +153,19 @@ Then add the NOT NULL constraint in separate migrations."
142
153
  # increased limit, but doesn't change number of length bytes
143
154
  # 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
144
155
  limit = options[:limit] || 255
145
- safe = ["varchar"].include?(sql_type) &&
156
+ safe = ["varchar"].include?(existing_type) &&
146
157
  limit >= existing_column.limit &&
147
158
  (limit <= 255 || existing_column.limit > 255)
148
159
  end
149
160
  end
150
161
  end
151
- raise_error :change_column unless safe
162
+
163
+ # unsafe to set NOT NULL for safe types
164
+ if safe && existing_column.null && options[:null] == false
165
+ raise_error :change_column_with_not_null
166
+ end
167
+
168
+ raise_error :change_column, rewrite_blocks: rewrite_blocks unless safe
152
169
  when :create_table
153
170
  table, options = args
154
171
  options ||= {}
@@ -176,7 +193,7 @@ Then add the NOT NULL constraint in separate migrations."
176
193
  end
177
194
 
178
195
  if options.delete(:foreign_key)
179
- headline = "Adding a validated foreign key locks the table."
196
+ headline = "Adding a foreign key blocks writes on both tables."
180
197
  append = "
181
198
 
182
199
  Then add the foreign key in separate migrations."
@@ -247,6 +264,10 @@ Then add the foreign key in separate migrations."
247
264
  validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
248
265
  end
249
266
  end
267
+ when :validate_foreign_key
268
+ if postgresql? && writes_blocked?
269
+ raise_error :validate_foreign_key
270
+ end
250
271
  end
251
272
 
252
273
  StrongMigrations.checks.each do |check|
@@ -259,8 +280,7 @@ Then add the foreign key in separate migrations."
259
280
  # outdated statistics + a new index can hurt performance of existing queries
260
281
  if StrongMigrations.auto_analyze && direction == :up && method == :add_index
261
282
  if postgresql?
262
- # TODO remove verbose in 0.7.0
263
- connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}"
283
+ connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}"
264
284
  elsif mariadb? || mysql?
265
285
  connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
266
286
  end
@@ -269,6 +289,8 @@ Then add the foreign key in separate migrations."
269
289
  result
270
290
  end
271
291
 
292
+ private
293
+
272
294
  def set_timeouts
273
295
  if !@timeouts_set
274
296
  if StrongMigrations.statement_timeout
@@ -303,8 +325,6 @@ Then add the foreign key in separate migrations."
303
325
  end
304
326
  end
305
327
 
306
- private
307
-
308
328
  def connection
309
329
  @migration.connection
310
330
  end
@@ -314,7 +334,8 @@ Then add the foreign key in separate migrations."
314
334
  end
315
335
 
316
336
  def safe?
317
- @safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) || direction == :down || version_safe?
337
+ @safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) ||
338
+ (direction == :down && !StrongMigrations.check_down) || version_safe?
318
339
  end
319
340
 
320
341
  def version_safe?
@@ -419,11 +440,16 @@ Then add the foreign key in separate migrations."
419
440
  end
420
441
 
421
442
  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
443
+ query = <<~SQL
444
+ SELECT
445
+ conname AS name,
446
+ pg_get_constraintdef(oid) AS def
447
+ FROM
448
+ pg_constraint
449
+ WHERE
450
+ contype = 'c' AND
451
+ convalidated AND
452
+ conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
427
453
  SQL
428
454
  connection.select_all(query.squish).to_a
429
455
  end
@@ -472,6 +498,23 @@ Then add the foreign key in separate migrations."
472
498
  "#{command} #{str_args.join(", ")}"
473
499
  end
474
500
 
501
+ def writes_blocked?
502
+ query = <<~SQL
503
+ SELECT
504
+ relation::regclass::text
505
+ FROM
506
+ pg_locks
507
+ WHERE
508
+ mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
509
+ pid = pg_backend_pid()
510
+ SQL
511
+ connection.select_all(query.squish).any?
512
+ end
513
+
514
+ def rewrite_blocks
515
+ mysql? || mariadb? ? "writes" : "reads and writes"
516
+ end
517
+
475
518
  def backfill_code(table, column, default)
476
519
  model = table.to_s.classify
477
520
  "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
@@ -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
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.6.8"
2
+ VERSION = "0.7.0"
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,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.8
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-05-14 00:00:00.000000000 Z
13
+ date: 2020-07-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord