strong_migrations 1.6.3 → 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 +9 -1
- data/README.md +92 -58
- data/lib/strong_migrations/checker.rb +7 -5
- data/lib/strong_migrations/checks.rb +15 -0
- data/lib/strong_migrations/error_messages.rb +20 -3
- data/lib/strong_migrations/migration.rb +8 -0
- 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: 0b621e800497f9e9c77e0208a899f7e8d17a944caa61dffdea80911e8ddd5ead
|
4
|
+
data.tar.gz: 3f7d4275c416db6b954c1c2e1109ad5118a1c0a7cc6eb25a81203f82a4e47b35
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0243834c1f5b12bc637a00b49f8b7eb773dcdd5b8592a354b705d72b6e5b6a728babfb12fc3d058093086529ebfe7de34abd9f1c834bc95323b01f8a48417445'
|
7
|
+
data.tar.gz: a24deb56f0adbe0206791b9155707489d03c02f021842f9284eaadcf5a2b99c30fe6f3ae2c51fef15fc5fef9bf7066b1a72f84c446ed8c12919c43f29eee9f95
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## 1.7.0 (2024-01-05)
|
2
|
+
|
3
|
+
- Added check for `add_unique_constraint`
|
4
|
+
|
5
|
+
## 1.6.4 (2023-10-17)
|
6
|
+
|
7
|
+
- Fixed false positives with `revert`
|
8
|
+
|
1
9
|
## 1.6.3 (2023-09-20)
|
2
10
|
|
3
11
|
- Added support for Trilogy
|
@@ -241,7 +249,7 @@ Other
|
|
241
249
|
## 0.2.2 (2018-02-14)
|
242
250
|
|
243
251
|
- Friendlier output
|
244
|
-
- Better method of hooking into
|
252
|
+
- Better method of hooking into Active Record
|
245
253
|
|
246
254
|
## 0.2.1 (2018-02-07)
|
247
255
|
|
data/README.md
CHANGED
@@ -8,7 +8,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
|
|
@@ -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
|
@@ -75,6 +75,7 @@ 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)
|
78
79
|
- [adding an exclusion constraint](#adding-an-exclusion-constraint)
|
79
80
|
- [adding a json column](#adding-a-json-column)
|
80
81
|
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
@@ -96,7 +97,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
|
|
96
97
|
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
97
98
|
|
98
99
|
```ruby
|
99
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.
|
100
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.1]
|
100
101
|
def change
|
101
102
|
remove_column :users, :some_column
|
102
103
|
end
|
@@ -117,7 +118,7 @@ end
|
|
117
118
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
118
119
|
|
119
120
|
```ruby
|
120
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.
|
121
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.1]
|
121
122
|
def change
|
122
123
|
safety_assured { remove_column :users, :some_column }
|
123
124
|
end
|
@@ -134,7 +135,7 @@ end
|
|
134
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.
|
135
136
|
|
136
137
|
```ruby
|
137
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[7.
|
138
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
138
139
|
def change
|
139
140
|
add_column :users, :some_column, :text, default: "default_value"
|
140
141
|
end
|
@@ -148,7 +149,7 @@ In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a t
|
|
148
149
|
Instead, add the column without a default value, then change the default.
|
149
150
|
|
150
151
|
```ruby
|
151
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[7.
|
152
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
152
153
|
def up
|
153
154
|
add_column :users, :some_column, :text
|
154
155
|
change_column_default :users, :some_column, "default_value"
|
@@ -169,7 +170,7 @@ See the next section for how to backfill.
|
|
169
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/).
|
170
171
|
|
171
172
|
```ruby
|
172
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[7.
|
173
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
173
174
|
def change
|
174
175
|
add_column :users, :some_column, :text
|
175
176
|
User.update_all some_column: "default_value"
|
@@ -184,7 +185,7 @@ Also, running a single query to update data can cause issues for large tables.
|
|
184
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!`.
|
185
186
|
|
186
187
|
```ruby
|
187
|
-
class BackfillSomeColumn < ActiveRecord::Migration[7.
|
188
|
+
class BackfillSomeColumn < ActiveRecord::Migration[7.1]
|
188
189
|
disable_ddl_transaction!
|
189
190
|
|
190
191
|
def up
|
@@ -203,7 +204,7 @@ end
|
|
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.
|
204
205
|
|
205
206
|
```ruby
|
206
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[7.
|
207
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
|
207
208
|
def change
|
208
209
|
add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
|
209
210
|
end
|
@@ -221,7 +222,7 @@ Add a non-generated column and use callbacks or triggers instead (or a virtual g
|
|
221
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.
|
222
223
|
|
223
224
|
```ruby
|
224
|
-
class ChangeSomeColumnType < ActiveRecord::Migration[7.
|
225
|
+
class ChangeSomeColumnType < ActiveRecord::Migration[7.1]
|
225
226
|
def change
|
226
227
|
change_column :users, :some_column, :new_type
|
227
228
|
end
|
@@ -267,7 +268,7 @@ A safer approach is to:
|
|
267
268
|
Renaming a column that’s in use will cause errors in your application.
|
268
269
|
|
269
270
|
```ruby
|
270
|
-
class RenameSomeColumn < ActiveRecord::Migration[7.
|
271
|
+
class RenameSomeColumn < ActiveRecord::Migration[7.1]
|
271
272
|
def change
|
272
273
|
rename_column :users, :some_column, :new_name
|
273
274
|
end
|
@@ -292,7 +293,7 @@ A safer approach is to:
|
|
292
293
|
Renaming a table that’s in use will cause errors in your application.
|
293
294
|
|
294
295
|
```ruby
|
295
|
-
class RenameUsersToCustomers < ActiveRecord::Migration[7.
|
296
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[7.1]
|
296
297
|
def change
|
297
298
|
rename_table :users, :customers
|
298
299
|
end
|
@@ -305,7 +306,7 @@ A safer approach is to:
|
|
305
306
|
|
306
307
|
1. Create a new table
|
307
308
|
2. Write to both tables
|
308
|
-
3. Backfill data from the old table to new table
|
309
|
+
3. Backfill data from the old table to the new table
|
309
310
|
4. Move reads from the old table to the new table
|
310
311
|
5. Stop writing to the old table
|
311
312
|
6. Drop the old table
|
@@ -317,7 +318,7 @@ A safer approach is to:
|
|
317
318
|
The `force` option can drop an existing table.
|
318
319
|
|
319
320
|
```ruby
|
320
|
-
class CreateUsers < ActiveRecord::Migration[7.
|
321
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
321
322
|
def change
|
322
323
|
create_table :users, force: true do |t|
|
323
324
|
# ...
|
@@ -331,7 +332,7 @@ end
|
|
331
332
|
Create tables without the `force` option.
|
332
333
|
|
333
334
|
```ruby
|
334
|
-
class CreateUsers < ActiveRecord::Migration[7.
|
335
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
335
336
|
def change
|
336
337
|
create_table :users do |t|
|
337
338
|
# ...
|
@@ -351,7 +352,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
351
352
|
Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
|
352
353
|
|
353
354
|
```ruby
|
354
|
-
class AddCheckConstraint < ActiveRecord::Migration[7.
|
355
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.1]
|
355
356
|
def change
|
356
357
|
add_check_constraint :users, "price > 0", name: "price_check"
|
357
358
|
end
|
@@ -363,7 +364,7 @@ end
|
|
363
364
|
Add the check constraint without validating existing rows:
|
364
365
|
|
365
366
|
```ruby
|
366
|
-
class AddCheckConstraint < ActiveRecord::Migration[7.
|
367
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.1]
|
367
368
|
def change
|
368
369
|
add_check_constraint :users, "price > 0", name: "price_check", validate: false
|
369
370
|
end
|
@@ -373,7 +374,7 @@ end
|
|
373
374
|
Then validate them in a separate migration.
|
374
375
|
|
375
376
|
```ruby
|
376
|
-
class ValidateCheckConstraint < ActiveRecord::Migration[7.
|
377
|
+
class ValidateCheckConstraint < ActiveRecord::Migration[7.1]
|
377
378
|
def change
|
378
379
|
validate_check_constraint :users, name: "price_check"
|
379
380
|
end
|
@@ -389,7 +390,7 @@ end
|
|
389
390
|
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
390
391
|
|
391
392
|
```ruby
|
392
|
-
class ExecuteSQL < ActiveRecord::Migration[7.
|
393
|
+
class ExecuteSQL < ActiveRecord::Migration[7.1]
|
393
394
|
def change
|
394
395
|
safety_assured { execute "..." }
|
395
396
|
end
|
@@ -405,7 +406,7 @@ end
|
|
405
406
|
In Postgres, adding an index non-concurrently blocks writes.
|
406
407
|
|
407
408
|
```ruby
|
408
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[7.
|
409
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
409
410
|
def change
|
410
411
|
add_index :users, :some_column
|
411
412
|
end
|
@@ -417,7 +418,7 @@ end
|
|
417
418
|
Add indexes concurrently.
|
418
419
|
|
419
420
|
```ruby
|
420
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[7.
|
421
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
421
422
|
disable_ddl_transaction!
|
422
423
|
|
423
424
|
def change
|
@@ -443,7 +444,7 @@ rails g index table column
|
|
443
444
|
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
444
445
|
|
445
446
|
```ruby
|
446
|
-
class AddReferenceToUsers < ActiveRecord::Migration[7.
|
447
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
447
448
|
def change
|
448
449
|
add_reference :users, :city
|
449
450
|
end
|
@@ -455,7 +456,7 @@ end
|
|
455
456
|
Make sure the index is added concurrently.
|
456
457
|
|
457
458
|
```ruby
|
458
|
-
class AddReferenceToUsers < ActiveRecord::Migration[7.
|
459
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
459
460
|
disable_ddl_transaction!
|
460
461
|
|
461
462
|
def change
|
@@ -473,7 +474,7 @@ end
|
|
473
474
|
In Postgres, adding a foreign key blocks writes on both tables.
|
474
475
|
|
475
476
|
```ruby
|
476
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.
|
477
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
477
478
|
def change
|
478
479
|
add_foreign_key :users, :orders
|
479
480
|
end
|
@@ -483,7 +484,7 @@ end
|
|
483
484
|
or
|
484
485
|
|
485
486
|
```ruby
|
486
|
-
class AddReferenceToUsers < ActiveRecord::Migration[7.
|
487
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.1]
|
487
488
|
def change
|
488
489
|
add_reference :users, :order, foreign_key: true
|
489
490
|
end
|
@@ -495,7 +496,7 @@ end
|
|
495
496
|
Add the foreign key without validating existing rows:
|
496
497
|
|
497
498
|
```ruby
|
498
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.
|
499
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
499
500
|
def change
|
500
501
|
add_foreign_key :users, :orders, validate: false
|
501
502
|
end
|
@@ -505,13 +506,46 @@ end
|
|
505
506
|
Then validate them in a separate migration.
|
506
507
|
|
507
508
|
```ruby
|
508
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[7.
|
509
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[7.1]
|
509
510
|
def change
|
510
511
|
validate_foreign_key :users, :orders
|
511
512
|
end
|
512
513
|
end
|
513
514
|
```
|
514
515
|
|
516
|
+
### Adding a unique constraint
|
517
|
+
|
518
|
+
#### Bad
|
519
|
+
|
520
|
+
In Postgres, adding a unique constraint creates a unique index, which blocks reads and writes.
|
521
|
+
|
522
|
+
```ruby
|
523
|
+
class AddUniqueContraint < ActiveRecord::Migration[7.1]
|
524
|
+
def change
|
525
|
+
add_unique_constraint :users, :some_column
|
526
|
+
end
|
527
|
+
end
|
528
|
+
```
|
529
|
+
|
530
|
+
#### Good
|
531
|
+
|
532
|
+
Create a unique index concurrently, then use it for the constraint.
|
533
|
+
|
534
|
+
```ruby
|
535
|
+
class AddUniqueContraint < ActiveRecord::Migration[7.1]
|
536
|
+
disable_ddl_transaction!
|
537
|
+
|
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
|
545
|
+
end
|
546
|
+
end
|
547
|
+
```
|
548
|
+
|
515
549
|
### Adding an exclusion constraint
|
516
550
|
|
517
551
|
#### Bad
|
@@ -537,7 +571,7 @@ end
|
|
537
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.
|
538
572
|
|
539
573
|
```ruby
|
540
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[7.
|
574
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[7.1]
|
541
575
|
def change
|
542
576
|
add_column :users, :properties, :json
|
543
577
|
end
|
@@ -549,7 +583,7 @@ end
|
|
549
583
|
Use `jsonb` instead.
|
550
584
|
|
551
585
|
```ruby
|
552
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[7.
|
586
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[7.1]
|
553
587
|
def change
|
554
588
|
add_column :users, :properties, :jsonb
|
555
589
|
end
|
@@ -565,7 +599,7 @@ end
|
|
565
599
|
In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
566
600
|
|
567
601
|
```ruby
|
568
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[7.
|
602
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
569
603
|
def change
|
570
604
|
change_column_null :users, :some_column, false
|
571
605
|
end
|
@@ -576,10 +610,10 @@ end
|
|
576
610
|
|
577
611
|
Instead, add a check constraint.
|
578
612
|
|
579
|
-
For Rails 6.1
|
613
|
+
For Rails 6.1+, use:
|
580
614
|
|
581
615
|
```ruby
|
582
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[7.
|
616
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
583
617
|
def change
|
584
618
|
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
585
619
|
end
|
@@ -600,10 +634,10 @@ end
|
|
600
634
|
|
601
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.
|
602
636
|
|
603
|
-
For Rails 6.1
|
637
|
+
For Rails 6.1+, use:
|
604
638
|
|
605
639
|
```ruby
|
606
|
-
class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.
|
640
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.1]
|
607
641
|
def change
|
608
642
|
validate_check_constraint :users, name: "users_some_column_null"
|
609
643
|
|
@@ -656,7 +690,7 @@ Disable partial writes in `config/application.rb`. For Rails < 7, use:
|
|
656
690
|
config.active_record.partial_writes = false
|
657
691
|
```
|
658
692
|
|
659
|
-
For Rails 7
|
693
|
+
For Rails 7+, use:
|
660
694
|
|
661
695
|
```ruby
|
662
696
|
config.active_record.partial_inserts = false
|
@@ -669,7 +703,7 @@ config.active_record.partial_inserts = false
|
|
669
703
|
Adding a non-unique index with more than three columns rarely improves performance.
|
670
704
|
|
671
705
|
```ruby
|
672
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[7.
|
706
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
673
707
|
def change
|
674
708
|
add_index :users, [:a, :b, :c, :d]
|
675
709
|
end
|
@@ -681,7 +715,7 @@ end
|
|
681
715
|
Instead, start an index with columns that narrow down the results the most.
|
682
716
|
|
683
717
|
```ruby
|
684
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[7.
|
718
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.1]
|
685
719
|
def change
|
686
720
|
add_index :users, [:b, :d]
|
687
721
|
end
|
@@ -695,7 +729,7 @@ For Postgres, be sure to add them concurrently.
|
|
695
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.
|
696
730
|
|
697
731
|
```ruby
|
698
|
-
class MySafeMigration < ActiveRecord::Migration[7.
|
732
|
+
class MySafeMigration < ActiveRecord::Migration[7.1]
|
699
733
|
def change
|
700
734
|
safety_assured { remove_column :users, :some_column }
|
701
735
|
end
|
@@ -793,25 +827,6 @@ ALTER ROLE myuser SET statement_timeout = '1h';
|
|
793
827
|
|
794
828
|
Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
|
795
829
|
|
796
|
-
## Lock Timeout Retries [experimental]
|
797
|
-
|
798
|
-
There’s the option to automatically retry statements when the lock timeout is reached. Here’s how it works:
|
799
|
-
|
800
|
-
- If a lock timeout happens outside a transaction, the statement is retried
|
801
|
-
- If it happens inside the DDL transaction, the entire migration is retried (only applicable to Postgres)
|
802
|
-
|
803
|
-
Add to `config/initializers/strong_migrations.rb`:
|
804
|
-
|
805
|
-
```ruby
|
806
|
-
StrongMigrations.lock_timeout_retries = 3
|
807
|
-
```
|
808
|
-
|
809
|
-
Set the delay between retries with:
|
810
|
-
|
811
|
-
```ruby
|
812
|
-
StrongMigrations.lock_timeout_retry_delay = 10.seconds
|
813
|
-
```
|
814
|
-
|
815
830
|
## App Timeouts
|
816
831
|
|
817
832
|
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.
|
@@ -855,12 +870,31 @@ production:
|
|
855
870
|
|
856
871
|
For HTTP connections, Redis, and other services, check out [this guide](https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts).
|
857
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
|
+
|
858
892
|
## Existing Migrations
|
859
893
|
|
860
894
|
To mark migrations as safe that were created before installing this gem, create an initializer with:
|
861
895
|
|
862
896
|
```ruby
|
863
|
-
StrongMigrations.start_after =
|
897
|
+
StrongMigrations.start_after = 20230101000000
|
864
898
|
```
|
865
899
|
|
866
900
|
Use the version from your latest migration.
|
@@ -48,6 +48,8 @@ module StrongMigrations
|
|
48
48
|
check_add_index(*args)
|
49
49
|
when :add_reference, :add_belongs_to
|
50
50
|
check_add_reference(method, *args)
|
51
|
+
when :add_unique_constraint
|
52
|
+
check_add_unique_constraint(*args)
|
51
53
|
when :change_column
|
52
54
|
check_change_column(*args)
|
53
55
|
when :change_column_default
|
@@ -123,6 +125,10 @@ module StrongMigrations
|
|
123
125
|
end
|
124
126
|
end
|
125
127
|
|
128
|
+
def version_safe?
|
129
|
+
version && version <= StrongMigrations.start_after
|
130
|
+
end
|
131
|
+
|
126
132
|
private
|
127
133
|
|
128
134
|
def check_version_supported
|
@@ -163,11 +169,7 @@ module StrongMigrations
|
|
163
169
|
end
|
164
170
|
|
165
171
|
def safe?
|
166
|
-
self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
|
167
|
-
end
|
168
|
-
|
169
|
-
def version_safe?
|
170
|
-
version && version <= StrongMigrations.start_after
|
172
|
+
self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
|
171
173
|
end
|
172
174
|
|
173
175
|
def version
|
@@ -180,6 +180,21 @@ Then add the foreign key in separate migrations."
|
|
180
180
|
end
|
181
181
|
end
|
182
182
|
|
183
|
+
def check_add_unique_constraint(*args)
|
184
|
+
args.extract_options!
|
185
|
+
table, column = args
|
186
|
+
|
187
|
+
# column and using_index cannot be used together
|
188
|
+
# check for column to ensure error message can be generated
|
189
|
+
if column && !new_table?(table)
|
190
|
+
index_name = connection.index_name(table, {column: column})
|
191
|
+
raise_error :add_unique_constraint,
|
192
|
+
index_command: command_str(:add_index, [table, column, {unique: true, algorithm: :concurrently}]),
|
193
|
+
constraint_command: command_str(:add_unique_constraint, [table, {using_index: index_name}]),
|
194
|
+
remove_command: command_str(:remove_unique_constraint, [table, column])
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
183
198
|
def check_change_column(*args)
|
184
199
|
options = args.extract_options!
|
185
200
|
table, column, type = args
|
@@ -88,7 +88,7 @@ in your application. A safer approach is to:
|
|
88
88
|
|
89
89
|
1. Create a new column
|
90
90
|
2. Write to both columns
|
91
|
-
3. Backfill data from the old column to new column
|
91
|
+
3. Backfill data from the old column to the new column
|
92
92
|
4. Move reads from the old column to the new column
|
93
93
|
5. Stop writing to the old column
|
94
94
|
6. Drop the old column",
|
@@ -99,7 +99,7 @@ in your application. A safer approach is to:
|
|
99
99
|
|
100
100
|
1. Create a new table. Don't forget to recreate indexes from the old table
|
101
101
|
2. Write to both tables
|
102
|
-
3. Backfill data from the old table to new table
|
102
|
+
3. Backfill data from the old table to the new table
|
103
103
|
4. Move reads from the old table to the new table
|
104
104
|
5. Stop writing to the old table
|
105
105
|
6. Drop the old table",
|
@@ -244,7 +244,24 @@ end",
|
|
244
244
|
Use disable_ddl_transaction! or a separate migration.",
|
245
245
|
|
246
246
|
add_exclusion_constraint:
|
247
|
-
"Adding an exclusion constraint blocks reads and writes while every row is checked."
|
247
|
+
"Adding an exclusion constraint blocks reads and writes while every row is checked.",
|
248
|
+
|
249
|
+
add_unique_constraint:
|
250
|
+
"Adding a unique constraint creates a unique index, which blocks reads and writes.
|
251
|
+
Instead, create a unique index concurrently, then use it for the constraint.
|
252
|
+
|
253
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
254
|
+
disable_ddl_transaction!
|
255
|
+
|
256
|
+
def up
|
257
|
+
%{index_command}
|
258
|
+
%{constraint_command}
|
259
|
+
end
|
260
|
+
|
261
|
+
def down
|
262
|
+
%{remove_command}
|
263
|
+
end
|
264
|
+
end"
|
248
265
|
}
|
249
266
|
self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
|
250
267
|
end
|
@@ -20,6 +20,14 @@ 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
32
|
strong_migrations_checker.class.safety_assured do
|
25
33
|
yield
|
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.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:
|
13
|
+
date: 2024-01-05 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.
|
78
|
+
rubygems_version: 3.5.3
|
79
79
|
signing_key:
|
80
80
|
specification_version: 4
|
81
81
|
summary: Catch unsafe migrations in development
|