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