strong_migrations 2.5.1 → 2.6.0
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 +10 -0
- data/LICENSE.txt +1 -1
- data/README.md +84 -56
- data/lib/generators/strong_migrations/install_generator.rb +2 -2
- data/lib/strong_migrations/checks.rb +92 -16
- data/lib/strong_migrations/error_messages.rb +18 -0
- data/lib/strong_migrations/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33d90fe56d07dcdf864739aeae07dac0c63b6504460299724bc293a43c5aeb7e
|
|
4
|
+
data.tar.gz: 9e7ea3be4a0d3b8b03bcb2e4e6b5f6b4425057388a4b8ae52a7263d5497d5de8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe975030ddbdd8bce5f8bc1a18498e67e505627618a47d201e916d6c0ca21f846852ef08dc923e3a8626c2634fdcfa4d38fe9b674bc13b58d7a0c5064a012f0a
|
|
7
|
+
data.tar.gz: 4e0fe03e8ac9cb3872a7d9179f5902a8044587ffab5b22c02c7bbf63303a9bee48e195c6b77f30491f382ff7435a22cfe9e92c0a6dc53f145cf67d1f21cfb109
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 2.6.0 (2026-04-07)
|
|
2
|
+
|
|
3
|
+
- Added check for `algorithm: :copy` with MySQL and MariaDB
|
|
4
|
+
- Added check for `lock: :shared` and `lock: :exclusive` with MySQL and MariaDB
|
|
5
|
+
- Dropped support for Ruby < 3.3 and Active Record < 7.2
|
|
6
|
+
|
|
7
|
+
## 2.5.2 (2025-12-20)
|
|
8
|
+
|
|
9
|
+
- Fixed false positive for `add_reference` with `foreign_key: {validate: false}`
|
|
10
|
+
|
|
1
11
|
## 2.5.1 (2025-10-13)
|
|
2
12
|
|
|
3
13
|
- Fixed `transaction_timeout` option with DDL transaction
|
data/LICENSE.txt
CHANGED
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[8.
|
|
46
|
+
class RemoveColumn < ActiveRecord::Migration[8.1]
|
|
47
47
|
def change
|
|
48
48
|
safety_assured { remove_column :users, :name }
|
|
49
49
|
end
|
|
@@ -82,9 +82,10 @@ Postgres-specific checks:
|
|
|
82
82
|
- [adding a column with a volatile default value](#adding-a-column-with-a-volatile-default-value)
|
|
83
83
|
- [renaming a schema](#renaming-a-schema)
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
MySQL and MariaDB-specific checks:
|
|
86
86
|
|
|
87
|
-
- [
|
|
87
|
+
- [using the COPY algorithm](#using-the-copy-algorithm)
|
|
88
|
+
- [using shared or exclusive locking](#using-shared-or-exclusive-locking)
|
|
88
89
|
|
|
89
90
|
Best practices:
|
|
90
91
|
|
|
@@ -99,7 +100,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
|
|
|
99
100
|
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
|
100
101
|
|
|
101
102
|
```ruby
|
|
102
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.
|
|
103
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.1]
|
|
103
104
|
def change
|
|
104
105
|
remove_column :users, :some_column
|
|
105
106
|
end
|
|
@@ -120,7 +121,7 @@ end
|
|
|
120
121
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
|
121
122
|
|
|
122
123
|
```ruby
|
|
123
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.
|
|
124
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.1]
|
|
124
125
|
def change
|
|
125
126
|
safety_assured { remove_column :users, :some_column }
|
|
126
127
|
end
|
|
@@ -137,7 +138,7 @@ end
|
|
|
137
138
|
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.
|
|
138
139
|
|
|
139
140
|
```ruby
|
|
140
|
-
class ChangeSomeColumnType < ActiveRecord::Migration[8.
|
|
141
|
+
class ChangeSomeColumnType < ActiveRecord::Migration[8.1]
|
|
141
142
|
def change
|
|
142
143
|
change_column :users, :some_column, :new_type
|
|
143
144
|
end
|
|
@@ -150,14 +151,14 @@ Type | Safe Changes
|
|
|
150
151
|
--- | ---
|
|
151
152
|
`cidr` | Changing to `inet`
|
|
152
153
|
`citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
|
|
153
|
-
`datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC
|
|
154
|
+
`datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC
|
|
154
155
|
`decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
|
155
156
|
`interval` | Increasing or removing `:precision`
|
|
156
157
|
`numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
|
157
158
|
`string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
|
|
158
159
|
`text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
|
|
159
160
|
`time` | Increasing or removing `:precision`
|
|
160
|
-
`timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC
|
|
161
|
+
`timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC
|
|
161
162
|
|
|
162
163
|
And some in MySQL and MariaDB:
|
|
163
164
|
|
|
@@ -183,7 +184,7 @@ A safer approach is to:
|
|
|
183
184
|
Renaming a column that’s in use will cause errors in your application.
|
|
184
185
|
|
|
185
186
|
```ruby
|
|
186
|
-
class RenameSomeColumn < ActiveRecord::Migration[8.
|
|
187
|
+
class RenameSomeColumn < ActiveRecord::Migration[8.1]
|
|
187
188
|
def change
|
|
188
189
|
rename_column :users, :some_column, :new_name
|
|
189
190
|
end
|
|
@@ -208,7 +209,7 @@ A safer approach is to:
|
|
|
208
209
|
Renaming a table that’s in use will cause errors in your application.
|
|
209
210
|
|
|
210
211
|
```ruby
|
|
211
|
-
class RenameUsersToCustomers < ActiveRecord::Migration[8.
|
|
212
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[8.1]
|
|
212
213
|
def change
|
|
213
214
|
rename_table :users, :customers
|
|
214
215
|
end
|
|
@@ -233,7 +234,7 @@ A safer approach is to:
|
|
|
233
234
|
The `force` option can drop an existing table.
|
|
234
235
|
|
|
235
236
|
```ruby
|
|
236
|
-
class CreateUsers < ActiveRecord::Migration[8.
|
|
237
|
+
class CreateUsers < ActiveRecord::Migration[8.1]
|
|
237
238
|
def change
|
|
238
239
|
create_table :users, force: true do |t|
|
|
239
240
|
# ...
|
|
@@ -247,7 +248,7 @@ end
|
|
|
247
248
|
Create tables without the `force` option.
|
|
248
249
|
|
|
249
250
|
```ruby
|
|
250
|
-
class CreateUsers < ActiveRecord::Migration[8.
|
|
251
|
+
class CreateUsers < ActiveRecord::Migration[8.1]
|
|
251
252
|
def change
|
|
252
253
|
create_table :users do |t|
|
|
253
254
|
# ...
|
|
@@ -265,7 +266,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
|
265
266
|
Adding an auto-incrementing column (`serial`/`bigserial` in Postgres and `AUTO_INCREMENT` in MySQL and MariaDB) 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.
|
|
266
267
|
|
|
267
268
|
```ruby
|
|
268
|
-
class AddIdToCitiesUsers < ActiveRecord::Migration[8.
|
|
269
|
+
class AddIdToCitiesUsers < ActiveRecord::Migration[8.1]
|
|
269
270
|
def change
|
|
270
271
|
add_column :cities_users, :id, :primary_key
|
|
271
272
|
end
|
|
@@ -285,7 +286,7 @@ Create a new table and migrate the data with the same steps as [renaming a table
|
|
|
285
286
|
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.
|
|
286
287
|
|
|
287
288
|
```ruby
|
|
288
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[8.
|
|
289
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
289
290
|
def change
|
|
290
291
|
add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
|
|
291
292
|
end
|
|
@@ -305,7 +306,7 @@ Add a non-generated column and use callbacks or triggers instead (or a virtual g
|
|
|
305
306
|
Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
|
|
306
307
|
|
|
307
308
|
```ruby
|
|
308
|
-
class AddCheckConstraint < ActiveRecord::Migration[8.
|
|
309
|
+
class AddCheckConstraint < ActiveRecord::Migration[8.1]
|
|
309
310
|
def change
|
|
310
311
|
add_check_constraint :users, "price > 0", name: "price_check"
|
|
311
312
|
end
|
|
@@ -317,7 +318,7 @@ end
|
|
|
317
318
|
Add the check constraint without validating existing rows:
|
|
318
319
|
|
|
319
320
|
```ruby
|
|
320
|
-
class AddCheckConstraint < ActiveRecord::Migration[8.
|
|
321
|
+
class AddCheckConstraint < ActiveRecord::Migration[8.1]
|
|
321
322
|
def change
|
|
322
323
|
add_check_constraint :users, "price > 0", name: "price_check", validate: false
|
|
323
324
|
end
|
|
@@ -327,7 +328,7 @@ end
|
|
|
327
328
|
Then validate them in a separate migration.
|
|
328
329
|
|
|
329
330
|
```ruby
|
|
330
|
-
class ValidateCheckConstraint < ActiveRecord::Migration[8.
|
|
331
|
+
class ValidateCheckConstraint < ActiveRecord::Migration[8.1]
|
|
331
332
|
def change
|
|
332
333
|
validate_check_constraint :users, name: "price_check"
|
|
333
334
|
end
|
|
@@ -343,7 +344,7 @@ end
|
|
|
343
344
|
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
|
344
345
|
|
|
345
346
|
```ruby
|
|
346
|
-
class ExecuteSQL < ActiveRecord::Migration[8.
|
|
347
|
+
class ExecuteSQL < ActiveRecord::Migration[8.1]
|
|
347
348
|
def change
|
|
348
349
|
safety_assured { execute "..." }
|
|
349
350
|
end
|
|
@@ -359,7 +360,7 @@ Note: Strong Migrations does not detect dangerous backfills.
|
|
|
359
360
|
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/).
|
|
360
361
|
|
|
361
362
|
```ruby
|
|
362
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[8.
|
|
363
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
363
364
|
def change
|
|
364
365
|
add_column :users, :some_column, :text
|
|
365
366
|
User.update_all some_column: "default_value"
|
|
@@ -374,7 +375,7 @@ Also, running a single query to update data can cause issues for large tables.
|
|
|
374
375
|
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!`.
|
|
375
376
|
|
|
376
377
|
```ruby
|
|
377
|
-
class BackfillSomeColumn < ActiveRecord::Migration[8.
|
|
378
|
+
class BackfillSomeColumn < ActiveRecord::Migration[8.1]
|
|
378
379
|
disable_ddl_transaction!
|
|
379
380
|
|
|
380
381
|
def up
|
|
@@ -397,7 +398,7 @@ Note: If backfilling with a method other than `update_all`, use `User.reset_colu
|
|
|
397
398
|
In Postgres, adding an index non-concurrently blocks writes.
|
|
398
399
|
|
|
399
400
|
```ruby
|
|
400
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[8.
|
|
401
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
|
|
401
402
|
def change
|
|
402
403
|
add_index :users, :some_column
|
|
403
404
|
end
|
|
@@ -409,7 +410,7 @@ end
|
|
|
409
410
|
Add indexes concurrently.
|
|
410
411
|
|
|
411
412
|
```ruby
|
|
412
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[8.
|
|
413
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
|
|
413
414
|
disable_ddl_transaction!
|
|
414
415
|
|
|
415
416
|
def change
|
|
@@ -435,7 +436,7 @@ rails g index table column
|
|
|
435
436
|
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
|
436
437
|
|
|
437
438
|
```ruby
|
|
438
|
-
class AddReferenceToUsers < ActiveRecord::Migration[8.
|
|
439
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.1]
|
|
439
440
|
def change
|
|
440
441
|
add_reference :users, :city
|
|
441
442
|
end
|
|
@@ -447,7 +448,7 @@ end
|
|
|
447
448
|
Make sure the index is added concurrently.
|
|
448
449
|
|
|
449
450
|
```ruby
|
|
450
|
-
class AddReferenceToUsers < ActiveRecord::Migration[8.
|
|
451
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.1]
|
|
451
452
|
disable_ddl_transaction!
|
|
452
453
|
|
|
453
454
|
def change
|
|
@@ -465,7 +466,7 @@ end
|
|
|
465
466
|
In Postgres, adding a foreign key blocks writes on both tables.
|
|
466
467
|
|
|
467
468
|
```ruby
|
|
468
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.
|
|
469
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
469
470
|
def change
|
|
470
471
|
add_foreign_key :users, :orders
|
|
471
472
|
end
|
|
@@ -475,7 +476,7 @@ end
|
|
|
475
476
|
or
|
|
476
477
|
|
|
477
478
|
```ruby
|
|
478
|
-
class AddReferenceToUsers < ActiveRecord::Migration[8.
|
|
479
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.1]
|
|
479
480
|
def change
|
|
480
481
|
add_reference :users, :order, foreign_key: true
|
|
481
482
|
end
|
|
@@ -487,7 +488,7 @@ end
|
|
|
487
488
|
Add the foreign key without validating existing rows:
|
|
488
489
|
|
|
489
490
|
```ruby
|
|
490
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.
|
|
491
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
491
492
|
def change
|
|
492
493
|
add_foreign_key :users, :orders, validate: false
|
|
493
494
|
end
|
|
@@ -497,7 +498,7 @@ end
|
|
|
497
498
|
Then validate them in a separate migration.
|
|
498
499
|
|
|
499
500
|
```ruby
|
|
500
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.
|
|
501
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
501
502
|
def change
|
|
502
503
|
validate_foreign_key :users, :orders
|
|
503
504
|
end
|
|
@@ -511,7 +512,7 @@ end
|
|
|
511
512
|
In Postgres, adding a unique constraint creates a unique index, which blocks reads and writes.
|
|
512
513
|
|
|
513
514
|
```ruby
|
|
514
|
-
class AddUniqueConstraint < ActiveRecord::Migration[8.
|
|
515
|
+
class AddUniqueConstraint < ActiveRecord::Migration[8.1]
|
|
515
516
|
def change
|
|
516
517
|
add_unique_constraint :users, :some_column
|
|
517
518
|
end
|
|
@@ -523,7 +524,7 @@ end
|
|
|
523
524
|
Create a unique index concurrently, then use it for the constraint.
|
|
524
525
|
|
|
525
526
|
```ruby
|
|
526
|
-
class AddUniqueConstraint < ActiveRecord::Migration[8.
|
|
527
|
+
class AddUniqueConstraint < ActiveRecord::Migration[8.1]
|
|
527
528
|
disable_ddl_transaction!
|
|
528
529
|
|
|
529
530
|
def up
|
|
@@ -544,7 +545,7 @@ end
|
|
|
544
545
|
In Postgres, adding an exclusion constraint blocks reads and writes while every row is checked.
|
|
545
546
|
|
|
546
547
|
```ruby
|
|
547
|
-
class AddExclusionConstraint < ActiveRecord::Migration[8.
|
|
548
|
+
class AddExclusionConstraint < ActiveRecord::Migration[8.1]
|
|
548
549
|
def change
|
|
549
550
|
add_exclusion_constraint :users, "number WITH =", using: :gist
|
|
550
551
|
end
|
|
@@ -562,7 +563,7 @@ end
|
|
|
562
563
|
In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
|
563
564
|
|
|
564
565
|
```ruby
|
|
565
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[8.
|
|
566
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[8.1]
|
|
566
567
|
def change
|
|
567
568
|
add_column :users, :properties, :json
|
|
568
569
|
end
|
|
@@ -574,7 +575,7 @@ end
|
|
|
574
575
|
Use `jsonb` instead.
|
|
575
576
|
|
|
576
577
|
```ruby
|
|
577
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[8.
|
|
578
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[8.1]
|
|
578
579
|
def change
|
|
579
580
|
add_column :users, :properties, :jsonb
|
|
580
581
|
end
|
|
@@ -590,7 +591,7 @@ end
|
|
|
590
591
|
In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
|
591
592
|
|
|
592
593
|
```ruby
|
|
593
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[8.
|
|
594
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[8.1]
|
|
594
595
|
def change
|
|
595
596
|
change_column_null :users, :some_column, false
|
|
596
597
|
end
|
|
@@ -602,7 +603,7 @@ end
|
|
|
602
603
|
Instead, add a check constraint.
|
|
603
604
|
|
|
604
605
|
```ruby
|
|
605
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[8.
|
|
606
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[8.1]
|
|
606
607
|
def change
|
|
607
608
|
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
|
608
609
|
end
|
|
@@ -612,7 +613,7 @@ end
|
|
|
612
613
|
Then validate it in a separate migration. Once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
|
|
613
614
|
|
|
614
615
|
```ruby
|
|
615
|
-
class ValidateSomeColumnNotNull < ActiveRecord::Migration[8.
|
|
616
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[8.1]
|
|
616
617
|
def up
|
|
617
618
|
validate_check_constraint :users, name: "users_some_column_null"
|
|
618
619
|
change_column_null :users, :some_column, false
|
|
@@ -633,7 +634,7 @@ end
|
|
|
633
634
|
Adding a column with a volatile default value to an existing table causes the entire table to be rewritten. During this time, reads and writes are blocked.
|
|
634
635
|
|
|
635
636
|
```ruby
|
|
636
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[8.
|
|
637
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
637
638
|
def change
|
|
638
639
|
add_column :users, :some_column, :uuid, default: "gen_random_uuid()"
|
|
639
640
|
end
|
|
@@ -645,7 +646,7 @@ end
|
|
|
645
646
|
Instead, add the column without a default value, then change the default.
|
|
646
647
|
|
|
647
648
|
```ruby
|
|
648
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[8.
|
|
649
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
649
650
|
def up
|
|
650
651
|
add_column :users, :some_column, :uuid
|
|
651
652
|
change_column_default :users, :some_column, from: nil, to: "gen_random_uuid()"
|
|
@@ -684,34 +685,56 @@ A safer approach is to:
|
|
|
684
685
|
5. Stop writing to the old schema
|
|
685
686
|
6. Drop the old schema
|
|
686
687
|
|
|
687
|
-
###
|
|
688
|
+
### Using the COPY algorithm
|
|
688
689
|
|
|
689
690
|
#### Bad
|
|
690
691
|
|
|
691
|
-
|
|
692
|
+
In MySQL and MariaDB, using the `COPY` algorithm blocks writes.
|
|
692
693
|
|
|
693
694
|
```ruby
|
|
694
|
-
class
|
|
695
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
|
|
695
696
|
def change
|
|
696
|
-
|
|
697
|
+
add_index :users, :some_column, algorithm: :copy
|
|
697
698
|
end
|
|
698
699
|
end
|
|
699
|
-
|
|
700
|
-
User.create!(some_column: "old") # can insert "new"
|
|
701
700
|
```
|
|
702
701
|
|
|
703
702
|
#### Good
|
|
704
703
|
|
|
705
|
-
|
|
704
|
+
Use the default algorithm.
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
|
|
708
|
+
def change
|
|
709
|
+
add_index :users, :some_column
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Using shared or exclusive locking
|
|
715
|
+
|
|
716
|
+
#### Bad
|
|
717
|
+
|
|
718
|
+
In MySQL and MariaDB, using shared locking blocks writes, and using exclusive locking blocks reads and writes.
|
|
706
719
|
|
|
707
720
|
```ruby
|
|
708
|
-
|
|
721
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.2]
|
|
722
|
+
def change
|
|
723
|
+
add_index :users, :some_column, lock: :shared
|
|
724
|
+
end
|
|
725
|
+
end
|
|
709
726
|
```
|
|
710
727
|
|
|
711
|
-
|
|
728
|
+
#### Good
|
|
729
|
+
|
|
730
|
+
Use the default locking or no locking.
|
|
712
731
|
|
|
713
732
|
```ruby
|
|
714
|
-
|
|
733
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.2]
|
|
734
|
+
def change
|
|
735
|
+
add_index :users, :some_column
|
|
736
|
+
end
|
|
737
|
+
end
|
|
715
738
|
```
|
|
716
739
|
|
|
717
740
|
### Keeping non-unique indexes to three columns or less
|
|
@@ -721,7 +744,7 @@ config.active_record.partial_inserts = false
|
|
|
721
744
|
Adding a non-unique index with more than three columns rarely improves performance.
|
|
722
745
|
|
|
723
746
|
```ruby
|
|
724
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[8.
|
|
747
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
|
|
725
748
|
def change
|
|
726
749
|
add_index :users, [:a, :b, :c, :d]
|
|
727
750
|
end
|
|
@@ -733,7 +756,7 @@ end
|
|
|
733
756
|
Instead, start an index with columns that narrow down the results the most.
|
|
734
757
|
|
|
735
758
|
```ruby
|
|
736
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[8.
|
|
759
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
|
|
737
760
|
def change
|
|
738
761
|
add_index :users, [:d, :b]
|
|
739
762
|
end
|
|
@@ -747,7 +770,7 @@ For Postgres, be sure to add them concurrently.
|
|
|
747
770
|
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.
|
|
748
771
|
|
|
749
772
|
```ruby
|
|
750
|
-
class MySafeMigration < ActiveRecord::Migration[8.
|
|
773
|
+
class MySafeMigration < ActiveRecord::Migration[8.1]
|
|
751
774
|
def change
|
|
752
775
|
safety_assured { remove_column :users, :some_column }
|
|
753
776
|
end
|
|
@@ -944,7 +967,7 @@ Use the version from your latest migration.
|
|
|
944
967
|
If your development database version is different from production, you can specify the production version so the right checks run in development.
|
|
945
968
|
|
|
946
969
|
```ruby
|
|
947
|
-
StrongMigrations.target_version =
|
|
970
|
+
StrongMigrations.target_version = 16
|
|
948
971
|
```
|
|
949
972
|
|
|
950
973
|
The major version works well for Postgres, while the major and minor version is recommended for MySQL and MariaDB.
|
|
@@ -954,7 +977,7 @@ For safety, this option only affects development and test environments. In other
|
|
|
954
977
|
If your app has multiple databases with different versions, you can use:
|
|
955
978
|
|
|
956
979
|
```ruby
|
|
957
|
-
StrongMigrations.target_version = {primary:
|
|
980
|
+
StrongMigrations.target_version = {primary: 16, catalog: 18}
|
|
958
981
|
```
|
|
959
982
|
|
|
960
983
|
## Analyze Tables
|
|
@@ -967,15 +990,20 @@ StrongMigrations.auto_analyze = true
|
|
|
967
990
|
|
|
968
991
|
## Faster Migrations
|
|
969
992
|
|
|
970
|
-
Only dump the schema when adding a new migration. If you use Git, add to `
|
|
993
|
+
Only dump the schema when adding a new migration. If you use Git, add to the end of your `Rakefile`:
|
|
971
994
|
|
|
972
995
|
```rb
|
|
973
|
-
|
|
996
|
+
task :faster_migrations do
|
|
997
|
+
ActiveRecord.dump_schema_after_migration = Rails.env.development? &&
|
|
998
|
+
`git status db/migrate/ --porcelain`.present?
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
task "db:migrate" => "faster_migrations"
|
|
974
1002
|
```
|
|
975
1003
|
|
|
976
1004
|
## Schema Sanity
|
|
977
1005
|
|
|
978
|
-
|
|
1006
|
+
With Active Record < 8.1, 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`:
|
|
979
1007
|
|
|
980
1008
|
```ruby
|
|
981
1009
|
StrongMigrations.alphabetize_schema = true
|
|
@@ -76,6 +76,11 @@ module StrongMigrations
|
|
|
76
76
|
rewrite_blocks: adapter.rewrite_blocks,
|
|
77
77
|
append: append
|
|
78
78
|
end
|
|
79
|
+
|
|
80
|
+
check_algorithm_option("add_column", *args, **options)
|
|
81
|
+
|
|
82
|
+
# not necessarily dangerous, but not necessary
|
|
83
|
+
check_lock_option("add_column", *args, **options)
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
def check_add_exclusion_constraint(*args)
|
|
@@ -138,6 +143,10 @@ module StrongMigrations
|
|
|
138
143
|
|
|
139
144
|
raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
|
|
140
145
|
end
|
|
146
|
+
|
|
147
|
+
check_algorithm_option("add_index", *args, **options)
|
|
148
|
+
|
|
149
|
+
check_lock_option("add_index", *args, **options)
|
|
141
150
|
end
|
|
142
151
|
|
|
143
152
|
def check_add_reference(method, *args)
|
|
@@ -147,11 +156,15 @@ module StrongMigrations
|
|
|
147
156
|
if postgresql?
|
|
148
157
|
index_value = options.fetch(:index, true)
|
|
149
158
|
concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently
|
|
150
|
-
|
|
159
|
+
index_unsafe = index_value && !concurrently_set
|
|
151
160
|
|
|
152
|
-
|
|
161
|
+
foreign_key_value = options[:foreign_key]
|
|
162
|
+
validate_false = foreign_key_value.is_a?(Hash) && foreign_key_value[:validate] == false
|
|
163
|
+
foreign_key_unsafe = foreign_key_value && !validate_false
|
|
164
|
+
|
|
165
|
+
if index_unsafe || foreign_key_unsafe
|
|
153
166
|
if index_value.is_a?(Hash)
|
|
154
|
-
options
|
|
167
|
+
options = options.merge(index: index_value.merge(algorithm: :concurrently))
|
|
155
168
|
elsif index_value
|
|
156
169
|
options = options.merge(index: {algorithm: :concurrently})
|
|
157
170
|
end
|
|
@@ -161,7 +174,8 @@ module StrongMigrations
|
|
|
161
174
|
throw :safe
|
|
162
175
|
end
|
|
163
176
|
|
|
164
|
-
if
|
|
177
|
+
if foreign_key_unsafe
|
|
178
|
+
options.delete(:foreign_key)
|
|
165
179
|
headline = "Adding a foreign key blocks writes on both tables."
|
|
166
180
|
append = "\n\nThen add the foreign key in separate migrations."
|
|
167
181
|
else
|
|
@@ -174,6 +188,41 @@ module StrongMigrations
|
|
|
174
188
|
append: append
|
|
175
189
|
end
|
|
176
190
|
end
|
|
191
|
+
|
|
192
|
+
check_algorithm_option("add_reference", *args, **options)
|
|
193
|
+
|
|
194
|
+
# not necessarily dangerous, but not necessary
|
|
195
|
+
check_lock_option("add_reference", *args, **options)
|
|
196
|
+
|
|
197
|
+
if (mysql? || mariadb?) && !new_table?(table)
|
|
198
|
+
index_value = options[:index]
|
|
199
|
+
copy_set = index_value.is_a?(Hash) && index_value[:algorithm] == :copy
|
|
200
|
+
if copy_set
|
|
201
|
+
index_value = index_value.except(:algorithm)
|
|
202
|
+
if index_value.empty?
|
|
203
|
+
options = options.except(:index)
|
|
204
|
+
else
|
|
205
|
+
options = options.merge(index: index_value)
|
|
206
|
+
end
|
|
207
|
+
raise_error :copy_algorithm, command: command_str("add_reference", args + [options])
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if ar_version >= 8.2
|
|
211
|
+
lock = index_value.is_a?(Hash) && index_value[:lock]
|
|
212
|
+
if [:shared, :exclusive].include?(lock)
|
|
213
|
+
index_value = index_value.except(:lock)
|
|
214
|
+
if index_value.empty?
|
|
215
|
+
options = options.except(:index)
|
|
216
|
+
else
|
|
217
|
+
options = options.merge(index: index_value)
|
|
218
|
+
end
|
|
219
|
+
raise_error :lock_option,
|
|
220
|
+
command: command_str(method, args + [options]),
|
|
221
|
+
lock_type: lock.to_s,
|
|
222
|
+
lock_blocks: lock == :shared ? "reads" : "reads and writes"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
177
226
|
end
|
|
178
227
|
|
|
179
228
|
def check_add_unique_constraint(*args)
|
|
@@ -185,9 +234,9 @@ module StrongMigrations
|
|
|
185
234
|
if column && !new_table?(table)
|
|
186
235
|
index_name = connection.index_name(table, {column: column})
|
|
187
236
|
raise_error :add_unique_constraint,
|
|
188
|
-
index_command: command_str(
|
|
189
|
-
constraint_command: command_str(
|
|
190
|
-
remove_command: command_str(
|
|
237
|
+
index_command: command_str("add_index", [table, column, {unique: true, algorithm: :concurrently}]),
|
|
238
|
+
constraint_command: command_str("add_unique_constraint", [table, {using_index: index_name}]),
|
|
239
|
+
remove_command: command_str("remove_unique_constraint", [table, column])
|
|
191
240
|
end
|
|
192
241
|
end
|
|
193
242
|
|
|
@@ -219,16 +268,16 @@ module StrongMigrations
|
|
|
219
268
|
if constraints.any?
|
|
220
269
|
change_commands = []
|
|
221
270
|
constraints.each do |c|
|
|
222
|
-
change_commands << command_str(
|
|
271
|
+
change_commands << command_str("remove_check_constraint", [table, c.expression, {name: c.name}])
|
|
223
272
|
end
|
|
224
|
-
change_commands << command_str(
|
|
273
|
+
change_commands << command_str("change_column", args + [options])
|
|
225
274
|
constraints.each do |c|
|
|
226
|
-
change_commands << command_str(
|
|
275
|
+
change_commands << command_str("add_check_constraint", [table, c.expression, {name: c.name, validate: false}])
|
|
227
276
|
end
|
|
228
277
|
|
|
229
278
|
validate_commands = []
|
|
230
279
|
constraints.each do |c|
|
|
231
|
-
validate_commands << command_str(
|
|
280
|
+
validate_commands << command_str("validate_check_constraint", [table, {name: c.name}])
|
|
232
281
|
end
|
|
233
282
|
|
|
234
283
|
raise_error :change_column_constraint,
|
|
@@ -236,6 +285,11 @@ module StrongMigrations
|
|
|
236
285
|
validate_constraint_code: validate_commands.join("\n ")
|
|
237
286
|
end
|
|
238
287
|
end
|
|
288
|
+
|
|
289
|
+
check_algorithm_option("change_column", *args, **options)
|
|
290
|
+
|
|
291
|
+
# not necessarily dangerous, but not necessary
|
|
292
|
+
check_lock_option("change_column", *args, **options)
|
|
239
293
|
end
|
|
240
294
|
|
|
241
295
|
def check_change_column_default(*args)
|
|
@@ -281,12 +335,12 @@ module StrongMigrations
|
|
|
281
335
|
throw :safe
|
|
282
336
|
end
|
|
283
337
|
|
|
284
|
-
add_constraint_code = command_str(
|
|
338
|
+
add_constraint_code = command_str("add_check_constraint", add_args)
|
|
285
339
|
|
|
286
|
-
up_code = String.new(command_str(
|
|
287
|
-
up_code << "\n #{command_str(
|
|
288
|
-
up_code << "\n #{command_str(
|
|
289
|
-
down_code = "#{add_constraint_code}\n #{command_str(
|
|
340
|
+
up_code = String.new(command_str("validate_check_constraint", validate_args))
|
|
341
|
+
up_code << "\n #{command_str("change_column_null", change_args)}"
|
|
342
|
+
up_code << "\n #{command_str("remove_check_constraint", remove_args)}"
|
|
343
|
+
down_code = "#{add_constraint_code}\n #{command_str("change_column_null", [table, column, true])}"
|
|
290
344
|
validate_constraint_code = "def up\n #{up_code}\n end\n\n def down\n #{down_code}\n end"
|
|
291
345
|
|
|
292
346
|
raise_error :change_column_null_postgresql,
|
|
@@ -332,6 +386,7 @@ module StrongMigrations
|
|
|
332
386
|
raise_error :execute, header: "Possibly dangerous operation"
|
|
333
387
|
end
|
|
334
388
|
|
|
389
|
+
# supports algorithm and lock options, but always raises
|
|
335
390
|
def check_remove_column(method, *args)
|
|
336
391
|
columns =
|
|
337
392
|
case method
|
|
@@ -378,8 +433,14 @@ module StrongMigrations
|
|
|
378
433
|
|
|
379
434
|
raise_error :remove_index, command: command_str("remove_index", args + [options.merge(algorithm: :concurrently)])
|
|
380
435
|
end
|
|
436
|
+
|
|
437
|
+
check_algorithm_option("remove_index", *args, **options)
|
|
438
|
+
|
|
439
|
+
# not necessarily dangerous, but not necessary
|
|
440
|
+
check_lock_option("remove_index", *args, **options)
|
|
381
441
|
end
|
|
382
442
|
|
|
443
|
+
# supports algorithm and lock options, but always raises
|
|
383
444
|
def check_rename_column
|
|
384
445
|
raise_error :rename_column
|
|
385
446
|
end
|
|
@@ -437,6 +498,21 @@ module StrongMigrations
|
|
|
437
498
|
@migration.stop!(message, header: header || "Dangerous operation detected")
|
|
438
499
|
end
|
|
439
500
|
|
|
501
|
+
def check_algorithm_option(method, *args, **options)
|
|
502
|
+
if (mysql? || mariadb?) && options[:algorithm] == :copy && !new_table?(args[0]) && (ar_version >= 8.2 || method == "add_index")
|
|
503
|
+
raise_error :copy_algorithm, command: command_str(method, args + [options.except(:algorithm)])
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def check_lock_option(method, *args, **options)
|
|
508
|
+
if (mysql? || mariadb?) && [:shared, :exclusive].include?(options[:lock]) && !new_table?(args[0]) && ar_version >= 8.2
|
|
509
|
+
raise_error :lock_option,
|
|
510
|
+
command: command_str(method, args + [options.except(:lock)]),
|
|
511
|
+
lock_type: options[:lock].to_s,
|
|
512
|
+
lock_blocks: options[:lock] == :shared ? "reads" : "reads and writes"
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
440
516
|
def constraint_str(statement, identifiers)
|
|
441
517
|
# not all identifiers are tables, but this method of quoting should be fine
|
|
442
518
|
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
|
@@ -281,6 +281,24 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
|
281
281
|
def down
|
|
282
282
|
%{remove_command}
|
|
283
283
|
end
|
|
284
|
+
end",
|
|
285
|
+
|
|
286
|
+
copy_algorithm:
|
|
287
|
+
"Using the COPY algorithm blocks writes. Instead, use:
|
|
288
|
+
|
|
289
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
290
|
+
def change
|
|
291
|
+
%{command}
|
|
292
|
+
end
|
|
293
|
+
end",
|
|
294
|
+
|
|
295
|
+
lock_option:
|
|
296
|
+
"Using %{lock_type} locking blocks %{lock_blocks}. Instead, use:
|
|
297
|
+
|
|
298
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
299
|
+
def change
|
|
300
|
+
%{command}
|
|
301
|
+
end
|
|
284
302
|
end"
|
|
285
303
|
}
|
|
286
304
|
self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
|
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: 2.
|
|
4
|
+
version: 2.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
@@ -17,14 +17,14 @@ dependencies:
|
|
|
17
17
|
requirements:
|
|
18
18
|
- - ">="
|
|
19
19
|
- !ruby/object:Gem::Version
|
|
20
|
-
version: '7.
|
|
20
|
+
version: '7.2'
|
|
21
21
|
type: :runtime
|
|
22
22
|
prerelease: false
|
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
|
24
24
|
requirements:
|
|
25
25
|
- - ">="
|
|
26
26
|
- !ruby/object:Gem::Version
|
|
27
|
-
version: '7.
|
|
27
|
+
version: '7.2'
|
|
28
28
|
email:
|
|
29
29
|
- andrew@ankane.org
|
|
30
30
|
- bob.remeika@gmail.com
|
|
@@ -65,14 +65,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
65
65
|
requirements:
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '3.
|
|
68
|
+
version: '3.3'
|
|
69
69
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
70
|
requirements:
|
|
71
71
|
- - ">="
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
73
|
version: '0'
|
|
74
74
|
requirements: []
|
|
75
|
-
rubygems_version:
|
|
75
|
+
rubygems_version: 4.0.6
|
|
76
76
|
specification_version: 4
|
|
77
77
|
summary: Catch unsafe migrations in development
|
|
78
78
|
test_files: []
|