strong_migrations 0.7.6 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +123 -1
- data/LICENSE.txt +1 -1
- data/README.md +255 -147
- data/lib/strong_migrations/adapters/abstract_adapter.rb +77 -0
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +32 -0
- data/lib/strong_migrations/adapters/mysql_adapter.rb +107 -0
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +230 -0
- data/lib/strong_migrations/checker.rb +131 -519
- data/lib/strong_migrations/checks.rb +498 -0
- data/lib/strong_migrations/database_tasks.rb +2 -1
- data/lib/strong_migrations/error_messages.rb +267 -0
- data/lib/strong_migrations/migration.rb +18 -3
- data/lib/strong_migrations/migrator.rb +19 -0
- data/lib/strong_migrations/safe_methods.rb +33 -29
- data/lib/strong_migrations/schema_dumper.rb +21 -0
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +38 -225
- data/lib/tasks/strong_migrations.rake +2 -7
- metadata +15 -78
- data/lib/strong_migrations/alphabetize_columns.rb +0 -11
data/README.md
CHANGED
@@ -4,18 +4,18 @@ Catch unsafe migrations in development
|
|
4
4
|
|
5
5
|
✓ Detects potentially dangerous operations<br /> ✓ Prevents them from running by default<br /> ✓ Provides instructions on safer ways to do what you want
|
6
6
|
|
7
|
-
Supports
|
7
|
+
Supports PostgreSQL, MySQL, and MariaDB
|
8
8
|
|
9
9
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
10
10
|
|
11
|
-
[![Build Status](https://github.com/ankane/strong_migrations/workflows/build/badge.svg
|
11
|
+
[![Build Status](https://github.com/ankane/strong_migrations/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/strong_migrations/actions)
|
12
12
|
|
13
13
|
## Installation
|
14
14
|
|
15
15
|
Add this line to your application’s Gemfile:
|
16
16
|
|
17
17
|
```ruby
|
18
|
-
gem
|
18
|
+
gem "strong_migrations"
|
19
19
|
```
|
20
20
|
|
21
21
|
And run:
|
@@ -43,7 +43,7 @@ end
|
|
43
43
|
|
44
44
|
Deploy the code, then wrap this step in a safety_assured { ... } block.
|
45
45
|
|
46
|
-
class RemoveColumn < ActiveRecord::Migration[
|
46
|
+
class RemoveColumn < ActiveRecord::Migration[7.1]
|
47
47
|
def change
|
48
48
|
safety_assured { remove_column :users, :name }
|
49
49
|
end
|
@@ -62,12 +62,12 @@ Potentially dangerous operations:
|
|
62
62
|
- [removing a column](#removing-a-column)
|
63
63
|
- [adding a column with a default value](#adding-a-column-with-a-default-value)
|
64
64
|
- [backfilling data](#backfilling-data)
|
65
|
+
- [adding a stored generated column](#adding-a-stored-generated-column)
|
65
66
|
- [changing the type of a column](#changing-the-type-of-a-column)
|
66
67
|
- [renaming a column](#renaming-a-column)
|
67
68
|
- [renaming a table](#renaming-a-table)
|
68
69
|
- [creating a table with the force option](#creating-a-table-with-the-force-option)
|
69
70
|
- [adding a check constraint](#adding-a-check-constraint)
|
70
|
-
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
71
71
|
- [executing SQL directly](#executing-SQL-directly)
|
72
72
|
|
73
73
|
Postgres-specific checks:
|
@@ -75,7 +75,14 @@ Postgres-specific checks:
|
|
75
75
|
- [adding an index non-concurrently](#adding-an-index-non-concurrently)
|
76
76
|
- [adding a reference](#adding-a-reference)
|
77
77
|
- [adding a foreign key](#adding-a-foreign-key)
|
78
|
+
- [adding a unique constraint](#adding-a-unique-constraint)
|
79
|
+
- [adding an exclusion constraint](#adding-an-exclusion-constraint)
|
78
80
|
- [adding a json column](#adding-a-json-column)
|
81
|
+
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
82
|
+
|
83
|
+
Config-specific checks:
|
84
|
+
|
85
|
+
- [changing the default value of a column](#changing-the-default-value-of-a-column)
|
79
86
|
|
80
87
|
Best practices:
|
81
88
|
|
@@ -90,7 +97,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
|
|
90
97
|
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
91
98
|
|
92
99
|
```ruby
|
93
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[
|
100
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.1]
|
94
101
|
def change
|
95
102
|
remove_column :users, :some_column
|
96
103
|
end
|
@@ -107,18 +114,19 @@ end
|
|
107
114
|
end
|
108
115
|
```
|
109
116
|
|
110
|
-
2. Deploy code
|
117
|
+
2. Deploy the code
|
111
118
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
112
119
|
|
113
120
|
```ruby
|
114
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[
|
121
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.1]
|
115
122
|
def change
|
116
123
|
safety_assured { remove_column :users, :some_column }
|
117
124
|
end
|
118
125
|
end
|
119
126
|
```
|
120
127
|
|
121
|
-
4. Deploy and run migration
|
128
|
+
4. Deploy and run the migration
|
129
|
+
5. Remove the line added in step 1
|
122
130
|
|
123
131
|
### Adding a column with a default value
|
124
132
|
|
@@ -127,21 +135,21 @@ end
|
|
127
135
|
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.
|
128
136
|
|
129
137
|
```ruby
|
130
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
138
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
131
139
|
def change
|
132
140
|
add_column :users, :some_column, :text, default: "default_value"
|
133
141
|
end
|
134
142
|
end
|
135
143
|
```
|
136
144
|
|
137
|
-
In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a table rewrite and is safe.
|
145
|
+
In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a table rewrite and is safe (except for volatile functions like `gen_random_uuid()`).
|
138
146
|
|
139
147
|
#### Good
|
140
148
|
|
141
149
|
Instead, add the column without a default value, then change the default.
|
142
150
|
|
143
151
|
```ruby
|
144
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
152
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
145
153
|
def up
|
146
154
|
add_column :users, :some_column, :text
|
147
155
|
change_column_default :users, :some_column, "default_value"
|
@@ -162,7 +170,7 @@ See the next section for how to backfill.
|
|
162
170
|
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/).
|
163
171
|
|
164
172
|
```ruby
|
165
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
173
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
166
174
|
def change
|
167
175
|
add_column :users, :some_column, :text
|
168
176
|
User.update_all some_column: "default_value"
|
@@ -177,7 +185,7 @@ Also, running a single query to update data can cause issues for large tables.
|
|
177
185
|
There are three keys to backfilling safely: batching, throttling, and running it outside a transaction. Use the Rails console or a separate migration with `disable_ddl_transaction!`.
|
178
186
|
|
179
187
|
```ruby
|
180
|
-
class BackfillSomeColumn < ActiveRecord::Migration[
|
188
|
+
class BackfillSomeColumn < ActiveRecord::Migration[7.1]
|
181
189
|
disable_ddl_transaction!
|
182
190
|
|
183
191
|
def up
|
@@ -189,6 +197,24 @@ class BackfillSomeColumn < ActiveRecord::Migration[6.1]
|
|
189
197
|
end
|
190
198
|
```
|
191
199
|
|
200
|
+
### Adding a stored generated column
|
201
|
+
|
202
|
+
#### Bad
|
203
|
+
|
204
|
+
Adding a stored generated 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.
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
208
|
+
def change
|
209
|
+
add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
|
210
|
+
end
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
#### Good
|
215
|
+
|
216
|
+
Add a non-generated column and use callbacks or triggers instead (or a virtual generated column with MySQL and MariaDB).
|
217
|
+
|
192
218
|
### Changing the type of a column
|
193
219
|
|
194
220
|
#### Bad
|
@@ -196,26 +222,33 @@ end
|
|
196
222
|
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.
|
197
223
|
|
198
224
|
```ruby
|
199
|
-
class ChangeSomeColumnType < ActiveRecord::Migration[
|
225
|
+
class ChangeSomeColumnType < ActiveRecord::Migration[7.1]
|
200
226
|
def change
|
201
227
|
change_column :users, :some_column, :new_type
|
202
228
|
end
|
203
229
|
end
|
204
230
|
```
|
205
231
|
|
206
|
-
|
232
|
+
Some changes don’t require a table rewrite and are safe in Postgres:
|
207
233
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
234
|
+
Type | Safe Changes
|
235
|
+
--- | ---
|
236
|
+
`cidr` | Changing to `inet`
|
237
|
+
`citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
|
238
|
+
`datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC in Postgres 12+
|
239
|
+
`decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
240
|
+
`interval` | Increasing or removing `:precision`
|
241
|
+
`numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
242
|
+
`string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
|
243
|
+
`text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
|
244
|
+
`time` | Increasing or removing `:precision`
|
245
|
+
`timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC in Postgres 12+
|
214
246
|
|
215
|
-
And
|
247
|
+
And some in MySQL and MariaDB:
|
216
248
|
|
217
|
-
|
218
|
-
|
249
|
+
Type | Safe Changes
|
250
|
+
--- | ---
|
251
|
+
`string` | Increasing `:limit` from under 63 up to 63, increasing `:limit` from over 63 to the max (the threshold can be different if using an encoding other than `utf8mb4` - for instance, it’s 85 for `utf8mb3` and 255 for `latin1`)
|
219
252
|
|
220
253
|
#### Good
|
221
254
|
|
@@ -235,7 +268,7 @@ A safer approach is to:
|
|
235
268
|
Renaming a column that’s in use will cause errors in your application.
|
236
269
|
|
237
270
|
```ruby
|
238
|
-
class RenameSomeColumn < ActiveRecord::Migration[
|
271
|
+
class RenameSomeColumn < ActiveRecord::Migration[7.1]
|
239
272
|
def change
|
240
273
|
rename_column :users, :some_column, :new_name
|
241
274
|
end
|
@@ -260,7 +293,7 @@ A safer approach is to:
|
|
260
293
|
Renaming a table that’s in use will cause errors in your application.
|
261
294
|
|
262
295
|
```ruby
|
263
|
-
class RenameUsersToCustomers < ActiveRecord::Migration[
|
296
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[7.1]
|
264
297
|
def change
|
265
298
|
rename_table :users, :customers
|
266
299
|
end
|
@@ -273,7 +306,7 @@ A safer approach is to:
|
|
273
306
|
|
274
307
|
1. Create a new table
|
275
308
|
2. Write to both tables
|
276
|
-
3. Backfill data from the old table to new table
|
309
|
+
3. Backfill data from the old table to the new table
|
277
310
|
4. Move reads from the old table to the new table
|
278
311
|
5. Stop writing to the old table
|
279
312
|
6. Drop the old table
|
@@ -285,7 +318,7 @@ A safer approach is to:
|
|
285
318
|
The `force` option can drop an existing table.
|
286
319
|
|
287
320
|
```ruby
|
288
|
-
class CreateUsers < ActiveRecord::Migration[
|
321
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
289
322
|
def change
|
290
323
|
create_table :users, force: true do |t|
|
291
324
|
# ...
|
@@ -299,7 +332,7 @@ end
|
|
299
332
|
Create tables without the `force` option.
|
300
333
|
|
301
334
|
```ruby
|
302
|
-
class CreateUsers < ActiveRecord::Migration[
|
335
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
303
336
|
def change
|
304
337
|
create_table :users do |t|
|
305
338
|
# ...
|
@@ -319,7 +352,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
319
352
|
Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
|
320
353
|
|
321
354
|
```ruby
|
322
|
-
class AddCheckConstraint < ActiveRecord::Migration[
|
355
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.1]
|
323
356
|
def change
|
324
357
|
add_check_constraint :users, "price > 0", name: "price_check"
|
325
358
|
end
|
@@ -331,7 +364,7 @@ end
|
|
331
364
|
Add the check constraint without validating existing rows:
|
332
365
|
|
333
366
|
```ruby
|
334
|
-
class AddCheckConstraint < ActiveRecord::Migration[
|
367
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.1]
|
335
368
|
def change
|
336
369
|
add_check_constraint :users, "price > 0", name: "price_check", validate: false
|
337
370
|
end
|
@@ -341,7 +374,7 @@ end
|
|
341
374
|
Then validate them in a separate migration.
|
342
375
|
|
343
376
|
```ruby
|
344
|
-
class ValidateCheckConstraint < ActiveRecord::Migration[
|
377
|
+
class ValidateCheckConstraint < ActiveRecord::Migration[7.1]
|
345
378
|
def change
|
346
379
|
validate_check_constraint :users, name: "price_check"
|
347
380
|
end
|
@@ -352,264 +385,315 @@ end
|
|
352
385
|
|
353
386
|
[Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this (check constraints can be added with `NOT ENFORCED`, but enforcing blocks writes).
|
354
387
|
|
355
|
-
###
|
388
|
+
### Executing SQL directly
|
389
|
+
|
390
|
+
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
391
|
+
|
392
|
+
```ruby
|
393
|
+
class ExecuteSQL < ActiveRecord::Migration[7.1]
|
394
|
+
def change
|
395
|
+
safety_assured { execute "..." }
|
396
|
+
end
|
397
|
+
end
|
398
|
+
```
|
399
|
+
|
400
|
+
### Adding an index non-concurrently
|
356
401
|
|
357
402
|
:turtle: Safe by default available
|
358
403
|
|
359
404
|
#### Bad
|
360
405
|
|
361
|
-
|
406
|
+
In Postgres, adding an index non-concurrently blocks writes.
|
362
407
|
|
363
408
|
```ruby
|
364
|
-
class
|
409
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
365
410
|
def change
|
366
|
-
|
411
|
+
add_index :users, :some_column
|
367
412
|
end
|
368
413
|
end
|
369
414
|
```
|
370
415
|
|
371
|
-
#### Good
|
372
|
-
|
373
|
-
Instead, add a check constraint.
|
416
|
+
#### Good
|
374
417
|
|
375
|
-
|
418
|
+
Add indexes concurrently.
|
376
419
|
|
377
420
|
```ruby
|
378
|
-
class
|
421
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
422
|
+
disable_ddl_transaction!
|
423
|
+
|
379
424
|
def change
|
380
|
-
|
425
|
+
add_index :users, :some_column, algorithm: :concurrently
|
381
426
|
end
|
382
427
|
end
|
383
428
|
```
|
384
429
|
|
385
|
-
|
430
|
+
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.
|
431
|
+
|
432
|
+
With [gindex](https://github.com/ankane/gindex), you can generate an index migration instantly with:
|
433
|
+
|
434
|
+
```sh
|
435
|
+
rails g index table column
|
436
|
+
```
|
437
|
+
|
438
|
+
### Adding a reference
|
439
|
+
|
440
|
+
:turtle: Safe by default available
|
441
|
+
|
442
|
+
#### Bad
|
443
|
+
|
444
|
+
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
386
445
|
|
387
446
|
```ruby
|
388
|
-
class
|
447
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
389
448
|
def change
|
390
|
-
|
391
|
-
execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
|
392
|
-
end
|
449
|
+
add_reference :users, :city
|
393
450
|
end
|
394
451
|
end
|
395
452
|
```
|
396
453
|
|
397
|
-
|
454
|
+
#### Good
|
398
455
|
|
399
|
-
|
456
|
+
Make sure the index is added concurrently.
|
400
457
|
|
401
458
|
```ruby
|
402
|
-
class
|
403
|
-
|
404
|
-
validate_check_constraint :users, name: "users_some_column_null"
|
459
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
460
|
+
disable_ddl_transaction!
|
405
461
|
|
406
|
-
|
407
|
-
|
408
|
-
remove_check_constraint :users, name: "users_some_column_null"
|
462
|
+
def change
|
463
|
+
add_reference :users, :city, index: {algorithm: :concurrently}
|
409
464
|
end
|
410
465
|
end
|
411
466
|
```
|
412
467
|
|
413
|
-
|
468
|
+
### Adding a foreign key
|
469
|
+
|
470
|
+
:turtle: Safe by default available
|
471
|
+
|
472
|
+
#### Bad
|
473
|
+
|
474
|
+
In Postgres, adding a foreign key blocks writes on both tables.
|
414
475
|
|
415
476
|
```ruby
|
416
|
-
class
|
477
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
417
478
|
def change
|
418
|
-
|
419
|
-
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
|
420
|
-
end
|
421
|
-
|
422
|
-
# in Postgres 12+, you can then safely set NOT NULL on the column
|
423
|
-
change_column_null :users, :some_column, false
|
424
|
-
safety_assured do
|
425
|
-
execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
|
426
|
-
end
|
479
|
+
add_foreign_key :users, :orders
|
427
480
|
end
|
428
481
|
end
|
429
482
|
```
|
430
483
|
|
431
|
-
|
484
|
+
or
|
432
485
|
|
433
|
-
|
486
|
+
```ruby
|
487
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
488
|
+
def change
|
489
|
+
add_reference :users, :order, foreign_key: true
|
490
|
+
end
|
491
|
+
end
|
492
|
+
```
|
434
493
|
|
435
|
-
|
494
|
+
#### Good
|
436
495
|
|
437
|
-
|
496
|
+
Add the foreign key without validating existing rows:
|
438
497
|
|
439
498
|
```ruby
|
440
|
-
class
|
499
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
441
500
|
def change
|
442
|
-
|
501
|
+
add_foreign_key :users, :orders, validate: false
|
443
502
|
end
|
444
503
|
end
|
445
504
|
```
|
446
505
|
|
447
|
-
|
506
|
+
Then validate them in a separate migration.
|
448
507
|
|
449
|
-
|
508
|
+
```ruby
|
509
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
510
|
+
def change
|
511
|
+
validate_foreign_key :users, :orders
|
512
|
+
end
|
513
|
+
end
|
514
|
+
```
|
515
|
+
|
516
|
+
### Adding a unique constraint
|
450
517
|
|
451
518
|
#### Bad
|
452
519
|
|
453
|
-
In Postgres, adding
|
520
|
+
In Postgres, adding a unique constraint creates a unique index, which blocks reads and writes.
|
454
521
|
|
455
522
|
```ruby
|
456
|
-
class
|
523
|
+
class AddUniqueContraint < ActiveRecord::Migration[7.1]
|
457
524
|
def change
|
458
|
-
|
525
|
+
add_unique_constraint :users, :some_column
|
459
526
|
end
|
460
527
|
end
|
461
528
|
```
|
462
529
|
|
463
530
|
#### Good
|
464
531
|
|
465
|
-
|
532
|
+
Create a unique index concurrently, then use it for the constraint.
|
466
533
|
|
467
534
|
```ruby
|
468
|
-
class
|
535
|
+
class AddUniqueContraint < ActiveRecord::Migration[7.1]
|
469
536
|
disable_ddl_transaction!
|
470
537
|
|
471
|
-
def
|
472
|
-
add_index :users, :some_column, algorithm: :concurrently
|
538
|
+
def up
|
539
|
+
add_index :users, :some_column, unique: true, algorithm: :concurrently
|
540
|
+
add_unique_constraint :users, using_index: "index_users_on_some_column"
|
541
|
+
end
|
542
|
+
|
543
|
+
def down
|
544
|
+
remove_unique_constraint :users, :some_column
|
473
545
|
end
|
474
546
|
end
|
475
547
|
```
|
476
548
|
|
477
|
-
|
549
|
+
### Adding an exclusion constraint
|
478
550
|
|
479
|
-
|
551
|
+
#### Bad
|
480
552
|
|
481
|
-
|
482
|
-
|
553
|
+
In Postgres, adding an exclusion constraint blocks reads and writes while every row is checked.
|
554
|
+
|
555
|
+
```ruby
|
556
|
+
class AddExclusionContraint < ActiveRecord::Migration[7.1]
|
557
|
+
def change
|
558
|
+
add_exclusion_constraint :users, "number WITH =", using: :gist
|
559
|
+
end
|
560
|
+
end
|
483
561
|
```
|
484
562
|
|
485
|
-
|
563
|
+
#### Good
|
486
564
|
|
487
|
-
|
565
|
+
[Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this (exclusion constraints cannot be marked `NOT VALID`).
|
566
|
+
|
567
|
+
### Adding a json column
|
488
568
|
|
489
569
|
#### Bad
|
490
570
|
|
491
|
-
|
571
|
+
In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
492
572
|
|
493
573
|
```ruby
|
494
|
-
class
|
574
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[7.1]
|
495
575
|
def change
|
496
|
-
|
576
|
+
add_column :users, :properties, :json
|
497
577
|
end
|
498
578
|
end
|
499
579
|
```
|
500
580
|
|
501
581
|
#### Good
|
502
582
|
|
503
|
-
|
583
|
+
Use `jsonb` instead.
|
504
584
|
|
505
585
|
```ruby
|
506
|
-
class
|
507
|
-
disable_ddl_transaction!
|
508
|
-
|
586
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[7.1]
|
509
587
|
def change
|
510
|
-
|
588
|
+
add_column :users, :properties, :jsonb
|
511
589
|
end
|
512
590
|
end
|
513
591
|
```
|
514
592
|
|
515
|
-
###
|
593
|
+
### Setting NOT NULL on an existing column
|
516
594
|
|
517
595
|
:turtle: Safe by default available
|
518
596
|
|
519
597
|
#### Bad
|
520
598
|
|
521
|
-
In Postgres,
|
599
|
+
In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
522
600
|
|
523
601
|
```ruby
|
524
|
-
class
|
602
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
525
603
|
def change
|
526
|
-
|
527
|
-
end
|
528
|
-
end
|
529
|
-
```
|
530
|
-
|
531
|
-
or
|
532
|
-
|
533
|
-
```ruby
|
534
|
-
class AddReferenceToUsers < ActiveRecord::Migration[6.1]
|
535
|
-
def change
|
536
|
-
add_reference :users, :order, foreign_key: true
|
604
|
+
change_column_null :users, :some_column, false
|
537
605
|
end
|
538
606
|
end
|
539
607
|
```
|
540
608
|
|
541
609
|
#### Good
|
542
610
|
|
543
|
-
|
611
|
+
Instead, add a check constraint.
|
544
612
|
|
545
|
-
For Rails
|
613
|
+
For Rails 6.1+, use:
|
546
614
|
|
547
615
|
```ruby
|
548
|
-
class
|
616
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
549
617
|
def change
|
550
|
-
|
618
|
+
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
551
619
|
end
|
552
620
|
end
|
553
621
|
```
|
554
622
|
|
555
|
-
|
623
|
+
For Rails < 6.1, use:
|
556
624
|
|
557
625
|
```ruby
|
558
|
-
class
|
626
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
559
627
|
def change
|
560
|
-
|
628
|
+
safety_assured do
|
629
|
+
execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
|
630
|
+
end
|
561
631
|
end
|
562
632
|
end
|
563
633
|
```
|
564
634
|
|
565
|
-
|
635
|
+
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 Rails < 6.1). In Postgres 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
|
636
|
+
|
637
|
+
For Rails 6.1+, use:
|
566
638
|
|
567
639
|
```ruby
|
568
|
-
class
|
640
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
569
641
|
def change
|
570
|
-
|
571
|
-
|
572
|
-
|
642
|
+
validate_check_constraint :users, name: "users_some_column_null"
|
643
|
+
|
644
|
+
# in Postgres 12+, you can then safely set NOT NULL on the column
|
645
|
+
change_column_null :users, :some_column, false
|
646
|
+
remove_check_constraint :users, name: "users_some_column_null"
|
573
647
|
end
|
574
648
|
end
|
575
649
|
```
|
576
650
|
|
577
|
-
|
651
|
+
For Rails < 6.1, use:
|
578
652
|
|
579
653
|
```ruby
|
580
|
-
class
|
654
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
581
655
|
def change
|
582
656
|
safety_assured do
|
583
|
-
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "
|
657
|
+
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
|
658
|
+
end
|
659
|
+
|
660
|
+
# in Postgres 12+, you can then safely set NOT NULL on the column
|
661
|
+
change_column_null :users, :some_column, false
|
662
|
+
safety_assured do
|
663
|
+
execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
|
584
664
|
end
|
585
665
|
end
|
586
666
|
end
|
587
667
|
```
|
588
668
|
|
589
|
-
###
|
669
|
+
### Changing the default value of a column
|
590
670
|
|
591
671
|
#### Bad
|
592
672
|
|
593
|
-
|
673
|
+
Rails < 7 enables partial writes by default, which can cause incorrect values to be inserted when changing the default value of a column.
|
594
674
|
|
595
675
|
```ruby
|
596
|
-
class
|
676
|
+
class ChangeSomeColumnDefault < ActiveRecord::Migration[6.1]
|
597
677
|
def change
|
598
|
-
|
678
|
+
change_column_default :users, :some_column, from: "old", to: "new"
|
599
679
|
end
|
600
680
|
end
|
681
|
+
|
682
|
+
User.create!(some_column: "old") # can insert "new"
|
601
683
|
```
|
602
684
|
|
603
685
|
#### Good
|
604
686
|
|
605
|
-
|
687
|
+
Disable partial writes in `config/application.rb`. For Rails < 7, use:
|
606
688
|
|
607
689
|
```ruby
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
690
|
+
config.active_record.partial_writes = false
|
691
|
+
```
|
692
|
+
|
693
|
+
For Rails 7+, use:
|
694
|
+
|
695
|
+
```ruby
|
696
|
+
config.active_record.partial_inserts = false
|
613
697
|
```
|
614
698
|
|
615
699
|
### Keeping non-unique indexes to three columns or less
|
@@ -619,7 +703,7 @@ end
|
|
619
703
|
Adding a non-unique index with more than three columns rarely improves performance.
|
620
704
|
|
621
705
|
```ruby
|
622
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
706
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
623
707
|
def change
|
624
708
|
add_index :users, [:a, :b, :c, :d]
|
625
709
|
end
|
@@ -631,7 +715,7 @@ end
|
|
631
715
|
Instead, start an index with columns that narrow down the results the most.
|
632
716
|
|
633
717
|
```ruby
|
634
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
718
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
635
719
|
def change
|
636
720
|
add_index :users, [:b, :d]
|
637
721
|
end
|
@@ -645,7 +729,7 @@ For Postgres, be sure to add them concurrently.
|
|
645
729
|
To mark a step in the migration as safe, despite using a method that might otherwise be dangerous, wrap it in a `safety_assured` block.
|
646
730
|
|
647
731
|
```ruby
|
648
|
-
class MySafeMigration < ActiveRecord::Migration[
|
732
|
+
class MySafeMigration < ActiveRecord::Migration[7.1]
|
649
733
|
def change
|
650
734
|
safety_assured { remove_column :users, :some_column }
|
651
735
|
end
|
@@ -703,7 +787,7 @@ Disable specific checks with:
|
|
703
787
|
StrongMigrations.disable_check(:add_index)
|
704
788
|
```
|
705
789
|
|
706
|
-
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
|
790
|
+
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
|
707
791
|
|
708
792
|
## Down Migrations / Rollbacks
|
709
793
|
|
@@ -721,7 +805,7 @@ To customize specific messages, create an initializer with:
|
|
721
805
|
StrongMigrations.error_messages[:add_column_default] = "Your custom instructions"
|
722
806
|
```
|
723
807
|
|
724
|
-
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
|
808
|
+
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
|
725
809
|
|
726
810
|
## Migration Timeouts
|
727
811
|
|
@@ -786,12 +870,31 @@ production:
|
|
786
870
|
|
787
871
|
For HTTP connections, Redis, and other services, check out [this guide](https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts).
|
788
872
|
|
873
|
+
## Lock Timeout Retries [experimental]
|
874
|
+
|
875
|
+
There’s the option to automatically retry statements for migrations when the lock timeout is reached. Here’s how it works:
|
876
|
+
|
877
|
+
- If a lock timeout happens outside a transaction, the statement is retried
|
878
|
+
- If it happens inside the DDL transaction, the entire migration is retried (only applicable to Postgres)
|
879
|
+
|
880
|
+
Add to `config/initializers/strong_migrations.rb`:
|
881
|
+
|
882
|
+
```ruby
|
883
|
+
StrongMigrations.lock_timeout_retries = 3
|
884
|
+
```
|
885
|
+
|
886
|
+
Set the delay between retries with:
|
887
|
+
|
888
|
+
```ruby
|
889
|
+
StrongMigrations.lock_timeout_retry_delay = 10.seconds
|
890
|
+
```
|
891
|
+
|
789
892
|
## Existing Migrations
|
790
893
|
|
791
894
|
To mark migrations as safe that were created before installing this gem, create an initializer with:
|
792
895
|
|
793
896
|
```ruby
|
794
|
-
StrongMigrations.start_after =
|
897
|
+
StrongMigrations.start_after = 20230101000000
|
795
898
|
```
|
796
899
|
|
797
900
|
Use the version from your latest migration.
|
@@ -808,6 +911,12 @@ The major version works well for Postgres, while the full version is recommended
|
|
808
911
|
|
809
912
|
For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
|
810
913
|
|
914
|
+
If your app has multiple databases with different versions, with Rails 6.1+, you can use:
|
915
|
+
|
916
|
+
```ruby
|
917
|
+
StrongMigrations.target_version = {primary: 13, catalog: 15}
|
918
|
+
```
|
919
|
+
|
811
920
|
## Analyze Tables
|
812
921
|
|
813
922
|
Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with:
|
@@ -818,19 +927,18 @@ StrongMigrations.auto_analyze = true
|
|
818
927
|
|
819
928
|
## Faster Migrations
|
820
929
|
|
821
|
-
Only dump the schema when adding a new migration. If you use Git,
|
930
|
+
Only dump the schema when adding a new migration. If you use Git, add to `config/environments/development.rb`:
|
822
931
|
|
823
|
-
```
|
824
|
-
|
825
|
-
`git status db/migrate/ --porcelain`.present?
|
932
|
+
```rb
|
933
|
+
config.active_record.dump_schema_after_migration = `git status db/migrate/ --porcelain`.present?
|
826
934
|
```
|
827
935
|
|
828
936
|
## Schema Sanity
|
829
937
|
|
830
|
-
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
|
938
|
+
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 `config/initializers/strong_migrations.rb`:
|
831
939
|
|
832
940
|
```ruby
|
833
|
-
|
941
|
+
StrongMigrations.alphabetize_schema = true
|
834
942
|
```
|
835
943
|
|
836
944
|
## Permissions
|