strong_migrations 1.4.4 → 1.6.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -1
- data/README.md +107 -54
- data/lib/strong_migrations/checker.rb +22 -14
- data/lib/strong_migrations/checks.rb +24 -1
- data/lib/strong_migrations/error_messages.rb +10 -0
- data/lib/strong_migrations/migration.rb +9 -1
- data/lib/strong_migrations/safe_methods.rb +3 -2
- data/lib/strong_migrations/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5399a374f21a5e61350adbe125184d43664bc654cd1583804e7b01c9d6240aab
|
4
|
+
data.tar.gz: 71e638db667d72b1fdf433eb27cc4f382a6472bd0f75885f5ae244990c1b768a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c8dbc405cb94e4b8eb92a08161cb8689996e7a6ba216b326db27a83151f5dd796d53c42e5b4dd2f8e0cb224f3f6c8f94ff81edd23063438a8143f0d36c507b9
|
7
|
+
data.tar.gz: 532f43609d8006f71e325a708a74c45df8161d5957a5173701c77cc22e7dd3111562cbc70b715c6f94c955d08cda0fd43cfc577b9d902cda8a9e1fedcd4693cc
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
## 1.6.4 (2023-10-17)
|
2
|
+
|
3
|
+
- Fixed false positives with `revert`
|
4
|
+
|
5
|
+
## 1.6.3 (2023-09-20)
|
6
|
+
|
7
|
+
- Added support for Trilogy
|
8
|
+
|
9
|
+
## 1.6.2 (2023-09-13)
|
10
|
+
|
11
|
+
- Fixed foreign key options with `add_reference` and `safe_by_default`
|
12
|
+
- Fixed `safety_assured` with `revert`
|
13
|
+
|
14
|
+
## 1.6.1 (2023-08-09)
|
15
|
+
|
16
|
+
- Fixed `safety_assured` for custom checks with `safe_by_default`
|
17
|
+
|
18
|
+
## 1.6.0 (2023-07-22)
|
19
|
+
|
20
|
+
- Added check for `change_column_default`
|
21
|
+
|
22
|
+
## 1.5.0 (2023-07-02)
|
23
|
+
|
24
|
+
- Added check for `add_column` with stored generated columns
|
25
|
+
- Fixed `add_reference` with `foreign_key` and `index: false`
|
26
|
+
|
1
27
|
## 1.4.4 (2023-03-08)
|
2
28
|
|
3
29
|
- Fixed `add_foreign_key` with `name` and `column` options with `safe_by_default`
|
@@ -219,7 +245,7 @@ Other
|
|
219
245
|
## 0.2.2 (2018-02-14)
|
220
246
|
|
221
247
|
- Friendlier output
|
222
|
-
- Better method of hooking into
|
248
|
+
- Better method of hooking into Active Record
|
223
249
|
|
224
250
|
## 0.2.1 (2018-02-07)
|
225
251
|
|
data/README.md
CHANGED
@@ -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[7.
|
46
|
+
class RemoveColumn < ActiveRecord::Migration[7.1]
|
47
47
|
def change
|
48
48
|
safety_assured { remove_column :users, :name }
|
49
49
|
end
|
@@ -62,6 +62,7 @@ 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)
|
@@ -78,6 +79,10 @@ Postgres-specific checks:
|
|
78
79
|
- [adding a json column](#adding-a-json-column)
|
79
80
|
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
80
81
|
|
82
|
+
Config-specific checks:
|
83
|
+
|
84
|
+
- [changing the default value of a column](#changing-the-default-value-of-a-column)
|
85
|
+
|
81
86
|
Best practices:
|
82
87
|
|
83
88
|
- [keeping non-unique indexes to three columns or less](#keeping-non-unique-indexes-to-three-columns-or-less)
|
@@ -91,7 +96,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
|
|
91
96
|
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
92
97
|
|
93
98
|
```ruby
|
94
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.
|
99
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.1]
|
95
100
|
def change
|
96
101
|
remove_column :users, :some_column
|
97
102
|
end
|
@@ -112,7 +117,7 @@ end
|
|
112
117
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
113
118
|
|
114
119
|
```ruby
|
115
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.
|
120
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.1]
|
116
121
|
def change
|
117
122
|
safety_assured { remove_column :users, :some_column }
|
118
123
|
end
|
@@ -129,7 +134,7 @@ end
|
|
129
134
|
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.
|
130
135
|
|
131
136
|
```ruby
|
132
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[7.
|
137
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
133
138
|
def change
|
134
139
|
add_column :users, :some_column, :text, default: "default_value"
|
135
140
|
end
|
@@ -143,7 +148,7 @@ In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a t
|
|
143
148
|
Instead, add the column without a default value, then change the default.
|
144
149
|
|
145
150
|
```ruby
|
146
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[7.
|
151
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
147
152
|
def up
|
148
153
|
add_column :users, :some_column, :text
|
149
154
|
change_column_default :users, :some_column, "default_value"
|
@@ -164,7 +169,7 @@ See the next section for how to backfill.
|
|
164
169
|
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/).
|
165
170
|
|
166
171
|
```ruby
|
167
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[7.
|
172
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
168
173
|
def change
|
169
174
|
add_column :users, :some_column, :text
|
170
175
|
User.update_all some_column: "default_value"
|
@@ -179,7 +184,7 @@ Also, running a single query to update data can cause issues for large tables.
|
|
179
184
|
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!`.
|
180
185
|
|
181
186
|
```ruby
|
182
|
-
class BackfillSomeColumn < ActiveRecord::Migration[7.
|
187
|
+
class BackfillSomeColumn < ActiveRecord::Migration[7.1]
|
183
188
|
disable_ddl_transaction!
|
184
189
|
|
185
190
|
def up
|
@@ -191,6 +196,24 @@ class BackfillSomeColumn < ActiveRecord::Migration[7.0]
|
|
191
196
|
end
|
192
197
|
```
|
193
198
|
|
199
|
+
### Adding a stored generated column
|
200
|
+
|
201
|
+
#### Bad
|
202
|
+
|
203
|
+
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.
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
207
|
+
def change
|
208
|
+
add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
|
209
|
+
end
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
#### Good
|
214
|
+
|
215
|
+
Add a non-generated column and use callbacks or triggers instead (or a virtual generated column with MySQL and MariaDB).
|
216
|
+
|
194
217
|
### Changing the type of a column
|
195
218
|
|
196
219
|
#### Bad
|
@@ -198,7 +221,7 @@ end
|
|
198
221
|
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.
|
199
222
|
|
200
223
|
```ruby
|
201
|
-
class ChangeSomeColumnType < ActiveRecord::Migration[7.
|
224
|
+
class ChangeSomeColumnType < ActiveRecord::Migration[7.1]
|
202
225
|
def change
|
203
226
|
change_column :users, :some_column, :new_type
|
204
227
|
end
|
@@ -244,7 +267,7 @@ A safer approach is to:
|
|
244
267
|
Renaming a column that’s in use will cause errors in your application.
|
245
268
|
|
246
269
|
```ruby
|
247
|
-
class RenameSomeColumn < ActiveRecord::Migration[7.
|
270
|
+
class RenameSomeColumn < ActiveRecord::Migration[7.1]
|
248
271
|
def change
|
249
272
|
rename_column :users, :some_column, :new_name
|
250
273
|
end
|
@@ -269,7 +292,7 @@ A safer approach is to:
|
|
269
292
|
Renaming a table that’s in use will cause errors in your application.
|
270
293
|
|
271
294
|
```ruby
|
272
|
-
class RenameUsersToCustomers < ActiveRecord::Migration[7.
|
295
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[7.1]
|
273
296
|
def change
|
274
297
|
rename_table :users, :customers
|
275
298
|
end
|
@@ -294,7 +317,7 @@ A safer approach is to:
|
|
294
317
|
The `force` option can drop an existing table.
|
295
318
|
|
296
319
|
```ruby
|
297
|
-
class CreateUsers < ActiveRecord::Migration[7.
|
320
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
298
321
|
def change
|
299
322
|
create_table :users, force: true do |t|
|
300
323
|
# ...
|
@@ -308,7 +331,7 @@ end
|
|
308
331
|
Create tables without the `force` option.
|
309
332
|
|
310
333
|
```ruby
|
311
|
-
class CreateUsers < ActiveRecord::Migration[7.
|
334
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
312
335
|
def change
|
313
336
|
create_table :users do |t|
|
314
337
|
# ...
|
@@ -328,7 +351,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
328
351
|
Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
|
329
352
|
|
330
353
|
```ruby
|
331
|
-
class AddCheckConstraint < ActiveRecord::Migration[7.
|
354
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.1]
|
332
355
|
def change
|
333
356
|
add_check_constraint :users, "price > 0", name: "price_check"
|
334
357
|
end
|
@@ -340,7 +363,7 @@ end
|
|
340
363
|
Add the check constraint without validating existing rows:
|
341
364
|
|
342
365
|
```ruby
|
343
|
-
class AddCheckConstraint < ActiveRecord::Migration[7.
|
366
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.1]
|
344
367
|
def change
|
345
368
|
add_check_constraint :users, "price > 0", name: "price_check", validate: false
|
346
369
|
end
|
@@ -350,7 +373,7 @@ end
|
|
350
373
|
Then validate them in a separate migration.
|
351
374
|
|
352
375
|
```ruby
|
353
|
-
class ValidateCheckConstraint < ActiveRecord::Migration[7.
|
376
|
+
class ValidateCheckConstraint < ActiveRecord::Migration[7.1]
|
354
377
|
def change
|
355
378
|
validate_check_constraint :users, name: "price_check"
|
356
379
|
end
|
@@ -366,7 +389,7 @@ end
|
|
366
389
|
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
367
390
|
|
368
391
|
```ruby
|
369
|
-
class ExecuteSQL < ActiveRecord::Migration[7.
|
392
|
+
class ExecuteSQL < ActiveRecord::Migration[7.1]
|
370
393
|
def change
|
371
394
|
safety_assured { execute "..." }
|
372
395
|
end
|
@@ -382,7 +405,7 @@ end
|
|
382
405
|
In Postgres, adding an index non-concurrently blocks writes.
|
383
406
|
|
384
407
|
```ruby
|
385
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[7.
|
408
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
386
409
|
def change
|
387
410
|
add_index :users, :some_column
|
388
411
|
end
|
@@ -394,7 +417,7 @@ end
|
|
394
417
|
Add indexes concurrently.
|
395
418
|
|
396
419
|
```ruby
|
397
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[7.
|
420
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
398
421
|
disable_ddl_transaction!
|
399
422
|
|
400
423
|
def change
|
@@ -420,7 +443,7 @@ rails g index table column
|
|
420
443
|
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
421
444
|
|
422
445
|
```ruby
|
423
|
-
class AddReferenceToUsers < ActiveRecord::Migration[7.
|
446
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
424
447
|
def change
|
425
448
|
add_reference :users, :city
|
426
449
|
end
|
@@ -432,7 +455,7 @@ end
|
|
432
455
|
Make sure the index is added concurrently.
|
433
456
|
|
434
457
|
```ruby
|
435
|
-
class AddReferenceToUsers < ActiveRecord::Migration[7.
|
458
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
436
459
|
disable_ddl_transaction!
|
437
460
|
|
438
461
|
def change
|
@@ -450,7 +473,7 @@ end
|
|
450
473
|
In Postgres, adding a foreign key blocks writes on both tables.
|
451
474
|
|
452
475
|
```ruby
|
453
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.
|
476
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
454
477
|
def change
|
455
478
|
add_foreign_key :users, :orders
|
456
479
|
end
|
@@ -460,7 +483,7 @@ end
|
|
460
483
|
or
|
461
484
|
|
462
485
|
```ruby
|
463
|
-
class AddReferenceToUsers < ActiveRecord::Migration[7.
|
486
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
464
487
|
def change
|
465
488
|
add_reference :users, :order, foreign_key: true
|
466
489
|
end
|
@@ -472,7 +495,7 @@ end
|
|
472
495
|
Add the foreign key without validating existing rows:
|
473
496
|
|
474
497
|
```ruby
|
475
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.
|
498
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
476
499
|
def change
|
477
500
|
add_foreign_key :users, :orders, validate: false
|
478
501
|
end
|
@@ -482,7 +505,7 @@ end
|
|
482
505
|
Then validate them in a separate migration.
|
483
506
|
|
484
507
|
```ruby
|
485
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[7.
|
508
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
486
509
|
def change
|
487
510
|
validate_foreign_key :users, :orders
|
488
511
|
end
|
@@ -514,7 +537,7 @@ end
|
|
514
537
|
In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
515
538
|
|
516
539
|
```ruby
|
517
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[7.
|
540
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[7.1]
|
518
541
|
def change
|
519
542
|
add_column :users, :properties, :json
|
520
543
|
end
|
@@ -526,7 +549,7 @@ end
|
|
526
549
|
Use `jsonb` instead.
|
527
550
|
|
528
551
|
```ruby
|
529
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[7.
|
552
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[7.1]
|
530
553
|
def change
|
531
554
|
add_column :users, :properties, :jsonb
|
532
555
|
end
|
@@ -542,7 +565,7 @@ end
|
|
542
565
|
In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
543
566
|
|
544
567
|
```ruby
|
545
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[7.
|
568
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
546
569
|
def change
|
547
570
|
change_column_null :users, :some_column, false
|
548
571
|
end
|
@@ -553,10 +576,10 @@ end
|
|
553
576
|
|
554
577
|
Instead, add a check constraint.
|
555
578
|
|
556
|
-
For Rails 6.1
|
579
|
+
For Rails 6.1+, use:
|
557
580
|
|
558
581
|
```ruby
|
559
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[7.
|
582
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
560
583
|
def change
|
561
584
|
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
562
585
|
end
|
@@ -577,10 +600,10 @@ end
|
|
577
600
|
|
578
601
|
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.
|
579
602
|
|
580
|
-
For Rails 6.1
|
603
|
+
For Rails 6.1+, use:
|
581
604
|
|
582
605
|
```ruby
|
583
|
-
class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.
|
606
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
584
607
|
def change
|
585
608
|
validate_check_constraint :users, name: "users_some_column_null"
|
586
609
|
|
@@ -609,6 +632,36 @@ class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
|
609
632
|
end
|
610
633
|
```
|
611
634
|
|
635
|
+
### Changing the default value of a column
|
636
|
+
|
637
|
+
#### Bad
|
638
|
+
|
639
|
+
Rails < 7 enables partial writes by default, which can cause incorrect values to be inserted when changing the default value of a column.
|
640
|
+
|
641
|
+
```ruby
|
642
|
+
class ChangeSomeColumnDefault < ActiveRecord::Migration[6.1]
|
643
|
+
def change
|
644
|
+
change_column_default :users, :some_column, from: "old", to: "new"
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
User.create!(some_column: "old") # can insert "new"
|
649
|
+
```
|
650
|
+
|
651
|
+
#### Good
|
652
|
+
|
653
|
+
Disable partial writes in `config/application.rb`. For Rails < 7, use:
|
654
|
+
|
655
|
+
```ruby
|
656
|
+
config.active_record.partial_writes = false
|
657
|
+
```
|
658
|
+
|
659
|
+
For Rails 7+, use:
|
660
|
+
|
661
|
+
```ruby
|
662
|
+
config.active_record.partial_inserts = false
|
663
|
+
```
|
664
|
+
|
612
665
|
### Keeping non-unique indexes to three columns or less
|
613
666
|
|
614
667
|
#### Bad
|
@@ -616,7 +669,7 @@ end
|
|
616
669
|
Adding a non-unique index with more than three columns rarely improves performance.
|
617
670
|
|
618
671
|
```ruby
|
619
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[7.
|
672
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
620
673
|
def change
|
621
674
|
add_index :users, [:a, :b, :c, :d]
|
622
675
|
end
|
@@ -628,7 +681,7 @@ end
|
|
628
681
|
Instead, start an index with columns that narrow down the results the most.
|
629
682
|
|
630
683
|
```ruby
|
631
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[7.
|
684
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
632
685
|
def change
|
633
686
|
add_index :users, [:b, :d]
|
634
687
|
end
|
@@ -642,7 +695,7 @@ For Postgres, be sure to add them concurrently.
|
|
642
695
|
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.
|
643
696
|
|
644
697
|
```ruby
|
645
|
-
class MySafeMigration < ActiveRecord::Migration[7.
|
698
|
+
class MySafeMigration < ActiveRecord::Migration[7.1]
|
646
699
|
def change
|
647
700
|
safety_assured { remove_column :users, :some_column }
|
648
701
|
end
|
@@ -740,25 +793,6 @@ ALTER ROLE myuser SET statement_timeout = '1h';
|
|
740
793
|
|
741
794
|
Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
|
742
795
|
|
743
|
-
## Lock Timeout Retries [experimental]
|
744
|
-
|
745
|
-
There’s the option to automatically retry statements when the lock timeout is reached. Here’s how it works:
|
746
|
-
|
747
|
-
- If a lock timeout happens outside a transaction, the statement is retried
|
748
|
-
- If it happens inside the DDL transaction, the entire migration is retried (only applicable to Postgres)
|
749
|
-
|
750
|
-
Add to `config/initializers/strong_migrations.rb`:
|
751
|
-
|
752
|
-
```ruby
|
753
|
-
StrongMigrations.lock_timeout_retries = 3
|
754
|
-
```
|
755
|
-
|
756
|
-
Set the delay between retries with:
|
757
|
-
|
758
|
-
```ruby
|
759
|
-
StrongMigrations.lock_timeout_retry_delay = 10.seconds
|
760
|
-
```
|
761
|
-
|
762
796
|
## App Timeouts
|
763
797
|
|
764
798
|
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.
|
@@ -802,12 +836,31 @@ production:
|
|
802
836
|
|
803
837
|
For HTTP connections, Redis, and other services, check out [this guide](https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts).
|
804
838
|
|
839
|
+
## Lock Timeout Retries [experimental]
|
840
|
+
|
841
|
+
There’s the option to automatically retry statements for migrations when the lock timeout is reached. Here’s how it works:
|
842
|
+
|
843
|
+
- If a lock timeout happens outside a transaction, the statement is retried
|
844
|
+
- If it happens inside the DDL transaction, the entire migration is retried (only applicable to Postgres)
|
845
|
+
|
846
|
+
Add to `config/initializers/strong_migrations.rb`:
|
847
|
+
|
848
|
+
```ruby
|
849
|
+
StrongMigrations.lock_timeout_retries = 3
|
850
|
+
```
|
851
|
+
|
852
|
+
Set the delay between retries with:
|
853
|
+
|
854
|
+
```ruby
|
855
|
+
StrongMigrations.lock_timeout_retry_delay = 10.seconds
|
856
|
+
```
|
857
|
+
|
805
858
|
## Existing Migrations
|
806
859
|
|
807
860
|
To mark migrations as safe that were created before installing this gem, create an initializer with:
|
808
861
|
|
809
862
|
```ruby
|
810
|
-
StrongMigrations.start_after =
|
863
|
+
StrongMigrations.start_after = 20230101000000
|
811
864
|
```
|
812
865
|
|
813
866
|
Use the version from your latest migration.
|
@@ -5,21 +5,25 @@ module StrongMigrations
|
|
5
5
|
|
6
6
|
attr_accessor :direction, :transaction_disabled, :timeouts_set
|
7
7
|
|
8
|
+
class << self
|
9
|
+
attr_accessor :safe
|
10
|
+
end
|
11
|
+
|
8
12
|
def initialize(migration)
|
9
13
|
@migration = migration
|
10
14
|
@new_tables = []
|
11
|
-
@
|
15
|
+
@new_columns = []
|
12
16
|
@timeouts_set = false
|
13
17
|
@committed = false
|
14
18
|
end
|
15
19
|
|
16
|
-
def safety_assured
|
17
|
-
previous_value =
|
20
|
+
def self.safety_assured
|
21
|
+
previous_value = safe
|
18
22
|
begin
|
19
|
-
|
23
|
+
self.safe = true
|
20
24
|
yield
|
21
25
|
ensure
|
22
|
-
|
26
|
+
self.safe = previous_value
|
23
27
|
end
|
24
28
|
end
|
25
29
|
|
@@ -46,6 +50,8 @@ module StrongMigrations
|
|
46
50
|
check_add_reference(method, *args)
|
47
51
|
when :change_column
|
48
52
|
check_change_column(*args)
|
53
|
+
when :change_column_default
|
54
|
+
check_change_column_default(*args)
|
49
55
|
when :change_column_null
|
50
56
|
check_change_column_null(*args)
|
51
57
|
when :change_table
|
@@ -75,9 +81,11 @@ module StrongMigrations
|
|
75
81
|
@committed = true
|
76
82
|
end
|
77
83
|
|
78
|
-
|
79
|
-
|
80
|
-
|
84
|
+
if !safe?
|
85
|
+
# custom checks
|
86
|
+
StrongMigrations.checks.each do |check|
|
87
|
+
@migration.instance_exec(method, args, &check)
|
88
|
+
end
|
81
89
|
end
|
82
90
|
end
|
83
91
|
|
@@ -115,6 +123,10 @@ module StrongMigrations
|
|
115
123
|
end
|
116
124
|
end
|
117
125
|
|
126
|
+
def version_safe?
|
127
|
+
version && version <= StrongMigrations.start_after
|
128
|
+
end
|
129
|
+
|
118
130
|
private
|
119
131
|
|
120
132
|
def check_version_supported
|
@@ -155,11 +167,7 @@ module StrongMigrations
|
|
155
167
|
end
|
156
168
|
|
157
169
|
def safe?
|
158
|
-
|
159
|
-
end
|
160
|
-
|
161
|
-
def version_safe?
|
162
|
-
version && version <= StrongMigrations.start_after
|
170
|
+
self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
|
163
171
|
end
|
164
172
|
|
165
173
|
def version
|
@@ -172,7 +180,7 @@ module StrongMigrations
|
|
172
180
|
case connection.adapter_name
|
173
181
|
when /postg/i # PostgreSQL, PostGIS
|
174
182
|
Adapters::PostgreSQLAdapter
|
175
|
-
when /mysql/i
|
183
|
+
when /mysql|trilogy/i
|
176
184
|
if connection.try(:mariadb?)
|
177
185
|
Adapters::MariaDBAdapter
|
178
186
|
else
|
@@ -32,6 +32,9 @@ module StrongMigrations
|
|
32
32
|
table, column, type = args
|
33
33
|
default = options[:default]
|
34
34
|
|
35
|
+
# keep track of new columns of change_column_default check
|
36
|
+
@new_columns << [table.to_s, column.to_s]
|
37
|
+
|
35
38
|
# Check key since DEFAULT NULL behaves differently from no default
|
36
39
|
#
|
37
40
|
# Also, Active Record has special case for uuid columns that allows function default values
|
@@ -71,6 +74,10 @@ Then add the NOT NULL constraint in separate migrations."
|
|
71
74
|
raise_error :add_column_json,
|
72
75
|
command: command_str("add_column", [table, column, :jsonb, options])
|
73
76
|
end
|
77
|
+
|
78
|
+
if type.to_s == "virtual" && options[:stored]
|
79
|
+
raise_error :add_column_generated_stored, rewrite_blocks: adapter.rewrite_blocks
|
80
|
+
end
|
74
81
|
end
|
75
82
|
|
76
83
|
def check_add_exclusion_constraint(*args)
|
@@ -147,7 +154,7 @@ Then add the NOT NULL constraint in separate migrations."
|
|
147
154
|
if bad_index || options[:foreign_key]
|
148
155
|
if index_value.is_a?(Hash)
|
149
156
|
options[:index] = options[:index].merge(algorithm: :concurrently)
|
150
|
-
|
157
|
+
elsif index_value
|
151
158
|
options = options.merge(index: {algorithm: :concurrently})
|
152
159
|
end
|
153
160
|
|
@@ -194,6 +201,18 @@ Then add the foreign key in separate migrations."
|
|
194
201
|
raise_error :change_column, rewrite_blocks: adapter.rewrite_blocks unless safe
|
195
202
|
end
|
196
203
|
|
204
|
+
def check_change_column_default(*args)
|
205
|
+
table, column, _default_or_changes = args
|
206
|
+
|
207
|
+
# just check ActiveRecord::Base, even though can override on model
|
208
|
+
partial_inserts = ar_version >= 7 ? ActiveRecord::Base.partial_inserts : ActiveRecord::Base.partial_writes
|
209
|
+
|
210
|
+
if partial_inserts && !new_column?(table, column)
|
211
|
+
raise_error :change_column_default,
|
212
|
+
config: ar_version >= 7 ? "partial_inserts" : "partial_writes"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
197
216
|
def check_change_column_null(*args)
|
198
217
|
table, column, null, default = args
|
199
218
|
if !null
|
@@ -456,5 +475,9 @@ Then add the foreign key in separate migrations."
|
|
456
475
|
def new_table?(table)
|
457
476
|
@new_tables.include?(table.to_s)
|
458
477
|
end
|
478
|
+
|
479
|
+
def new_column?(table, column)
|
480
|
+
new_table?(table) || @new_columns.include?([table.to_s, column.to_s])
|
481
|
+
end
|
459
482
|
end
|
460
483
|
end
|
@@ -50,6 +50,9 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
50
50
|
end
|
51
51
|
end",
|
52
52
|
|
53
|
+
add_column_generated_stored:
|
54
|
+
"Adding a stored generated column blocks %{rewrite_blocks} while the entire table is rewritten.",
|
55
|
+
|
53
56
|
change_column:
|
54
57
|
"Changing the type of an existing column blocks %{rewrite_blocks}
|
55
58
|
while the entire table is rewritten. A safer approach is to:
|
@@ -158,6 +161,13 @@ Otherwise, remove the force option.",
|
|
158
161
|
execute call, so cannot help you here. Please make really sure that what
|
159
162
|
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
|
160
163
|
|
164
|
+
change_column_default:
|
165
|
+
"Partial writes are enabled, which can cause incorrect values
|
166
|
+
to be inserted when changing the default value of a column.
|
167
|
+
Disable partial writes in config/application.rb:
|
168
|
+
|
169
|
+
config.active_record.%{config} = false",
|
170
|
+
|
161
171
|
change_column_null:
|
162
172
|
"Passing a default value to change_column_null runs a single UPDATE query,
|
163
173
|
which can cause downtime. Instead, backfill the existing rows in the
|
@@ -20,8 +20,16 @@ module StrongMigrations
|
|
20
20
|
end
|
21
21
|
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
22
22
|
|
23
|
+
def revert(*)
|
24
|
+
if strong_migrations_checker.version_safe?
|
25
|
+
safety_assured { super }
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
23
31
|
def safety_assured
|
24
|
-
strong_migrations_checker.safety_assured do
|
32
|
+
strong_migrations_checker.class.safety_assured do
|
25
33
|
yield
|
26
34
|
end
|
27
35
|
end
|
@@ -30,10 +30,11 @@ module StrongMigrations
|
|
30
30
|
(ActiveRecord::Base.pluralize_table_names ? reference.to_s.pluralize : reference).to_sym
|
31
31
|
end
|
32
32
|
|
33
|
+
foreign_key_opts = foreign_key.is_a?(Hash) ? foreign_key.except(:to_table) : {}
|
33
34
|
if reference
|
34
|
-
@migration.add_foreign_key(table, name, column: "#{reference}_id")
|
35
|
+
@migration.add_foreign_key(table, name, column: "#{reference}_id", **foreign_key_opts)
|
35
36
|
else
|
36
|
-
@migration.add_foreign_key(table, name)
|
37
|
+
@migration.add_foreign_key(table, name, **foreign_key_opts)
|
37
38
|
end
|
38
39
|
end
|
39
40
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strong_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.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: 2023-
|
13
|
+
date: 2023-10-17 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: '0'
|
77
77
|
requirements: []
|
78
|
-
rubygems_version: 3.4.
|
78
|
+
rubygems_version: 3.4.10
|
79
79
|
signing_key:
|
80
80
|
specification_version: 4
|
81
81
|
summary: Catch unsafe migrations in development
|