strong_migrations 1.8.0 → 2.3.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 +38 -0
- data/LICENSE.txt +1 -1
- data/README.md +91 -93
- data/lib/generators/strong_migrations/install_generator.rb +3 -7
- data/lib/strong_migrations/adapters/abstract_adapter.rb +5 -10
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +1 -1
- data/lib/strong_migrations/adapters/mysql_adapter.rb +9 -4
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +24 -21
- data/lib/strong_migrations/checker.rb +70 -4
- data/lib/strong_migrations/checks.rb +80 -80
- data/lib/strong_migrations/error_messages.rb +17 -11
- data/lib/strong_migrations/migration.rb +2 -1
- data/lib/strong_migrations/{database_tasks.rb → migration_context.rb} +20 -3
- data/lib/strong_migrations/migrator.rb +6 -4
- data/lib/strong_migrations/safe_methods.rb +59 -38
- data/lib/strong_migrations/schema_dumper.rb +15 -4
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +9 -6
- metadata +7 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3538811c535186c2757c2e92a56ec753c77842a99728a727aabe323dd8852182
|
|
4
|
+
data.tar.gz: 6d33a69582e57e3035d7ef540894ce0020957aef8d4e081e693552615ac9af8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50a4b8ea0d0677bc0d5a3fa740f9f2e8b00acee0e673abaf7197c0c89540165ecb0cad913ed4244b61e3ac23342d4c44b8209991dea3841ee6dd7d7fbef827e3
|
|
7
|
+
data.tar.gz: fd6d44273d6365ca9e33a246e072a24da93191bfc73b5d425df22c0b3b6523ce218d6aa6360dd6a8fcbcf88b5a06bf41c7221ccdceb69f26b734510cd38876f7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
## 2.3.0 (2025-04-03)
|
|
2
|
+
|
|
3
|
+
- Added check for `change_column` for columns with check constraints with Postgres
|
|
4
|
+
|
|
5
|
+
## 2.2.1 (2025-03-21)
|
|
6
|
+
|
|
7
|
+
- Added support for `change_column_null` with default value with `safe_by_default` option
|
|
8
|
+
- Improved backfill instructions
|
|
9
|
+
- Fixed `safe_by_default` applying to migrations before `start_after`
|
|
10
|
+
|
|
11
|
+
## 2.2.0 (2025-02-01)
|
|
12
|
+
|
|
13
|
+
- Fixed constraint name for long table and column names with `change_column_null`
|
|
14
|
+
- Dropped support for Active Record < 7
|
|
15
|
+
|
|
16
|
+
## 2.1.0 (2024-11-08)
|
|
17
|
+
|
|
18
|
+
- Added `skip_database` method
|
|
19
|
+
- Added experimental `remove_invalid_indexes` option
|
|
20
|
+
- Added warning for unsupported adapters
|
|
21
|
+
- Improved output for `db:forward`, `db:rollback`, `db:migrate:up`, and `db:migrate:down`
|
|
22
|
+
- Made operations more retriable with `safe_by_default`
|
|
23
|
+
|
|
24
|
+
## 2.0.2 (2024-10-30)
|
|
25
|
+
|
|
26
|
+
- Fixed migrations not running with Active Record 8 rc2
|
|
27
|
+
|
|
28
|
+
## 2.0.1 (2024-10-14)
|
|
29
|
+
|
|
30
|
+
- Fixed issue with `alphabetize_schema` and virtual columns
|
|
31
|
+
|
|
32
|
+
## 2.0.0 (2024-06-28)
|
|
33
|
+
|
|
34
|
+
- Improved install generator for Trilogy
|
|
35
|
+
- Fixed charset check for MariaDB 11.4
|
|
36
|
+
- Dropped support for Ruby < 3.1 and Active Record < 6.1
|
|
37
|
+
- Dropped support for Postgres < 12, MySQL < 8.0, and MariaDB < 10.5
|
|
38
|
+
|
|
1
39
|
## 1.8.0 (2024-03-11)
|
|
2
40
|
|
|
3
41
|
- Added check for `add_column` with auto-incrementing columns
|
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[
|
|
46
|
+
class RemoveColumn < ActiveRecord::Migration[8.0]
|
|
47
47
|
def change
|
|
48
48
|
safety_assured { remove_column :users, :name }
|
|
49
49
|
end
|
|
@@ -64,7 +64,7 @@ Potentially dangerous operations:
|
|
|
64
64
|
- [renaming a column](#renaming-a-column)
|
|
65
65
|
- [renaming a table](#renaming-a-table)
|
|
66
66
|
- [creating a table with the force option](#creating-a-table-with-the-force-option)
|
|
67
|
-
- [adding an auto-incrementing column](#adding-an-auto-incrementing-column)
|
|
67
|
+
- [adding an auto-incrementing column](#adding-an-auto-incrementing-column)
|
|
68
68
|
- [adding a stored generated column](#adding-a-stored-generated-column)
|
|
69
69
|
- [adding a check constraint](#adding-a-check-constraint)
|
|
70
70
|
- [executing SQL directly](#executing-SQL-directly)
|
|
@@ -79,7 +79,7 @@ Postgres-specific checks:
|
|
|
79
79
|
- [adding an exclusion constraint](#adding-an-exclusion-constraint)
|
|
80
80
|
- [adding a json column](#adding-a-json-column)
|
|
81
81
|
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
|
82
|
-
- [adding a column with a default value](#adding-a-column-with-a-default-value)
|
|
82
|
+
- [adding a column with a volatile default value](#adding-a-column-with-a-volatile-default-value)
|
|
83
83
|
|
|
84
84
|
Config-specific checks:
|
|
85
85
|
|
|
@@ -98,7 +98,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
|
|
|
98
98
|
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
|
99
99
|
|
|
100
100
|
```ruby
|
|
101
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[
|
|
101
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.0]
|
|
102
102
|
def change
|
|
103
103
|
remove_column :users, :some_column
|
|
104
104
|
end
|
|
@@ -119,7 +119,7 @@ end
|
|
|
119
119
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
|
120
120
|
|
|
121
121
|
```ruby
|
|
122
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[
|
|
122
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.0]
|
|
123
123
|
def change
|
|
124
124
|
safety_assured { remove_column :users, :some_column }
|
|
125
125
|
end
|
|
@@ -136,7 +136,7 @@ end
|
|
|
136
136
|
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.
|
|
137
137
|
|
|
138
138
|
```ruby
|
|
139
|
-
class ChangeSomeColumnType < ActiveRecord::Migration[
|
|
139
|
+
class ChangeSomeColumnType < ActiveRecord::Migration[8.0]
|
|
140
140
|
def change
|
|
141
141
|
change_column :users, :some_column, :new_type
|
|
142
142
|
end
|
|
@@ -182,7 +182,7 @@ A safer approach is to:
|
|
|
182
182
|
Renaming a column that’s in use will cause errors in your application.
|
|
183
183
|
|
|
184
184
|
```ruby
|
|
185
|
-
class RenameSomeColumn < ActiveRecord::Migration[
|
|
185
|
+
class RenameSomeColumn < ActiveRecord::Migration[8.0]
|
|
186
186
|
def change
|
|
187
187
|
rename_column :users, :some_column, :new_name
|
|
188
188
|
end
|
|
@@ -207,7 +207,7 @@ A safer approach is to:
|
|
|
207
207
|
Renaming a table that’s in use will cause errors in your application.
|
|
208
208
|
|
|
209
209
|
```ruby
|
|
210
|
-
class RenameUsersToCustomers < ActiveRecord::Migration[
|
|
210
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[8.0]
|
|
211
211
|
def change
|
|
212
212
|
rename_table :users, :customers
|
|
213
213
|
end
|
|
@@ -232,7 +232,7 @@ A safer approach is to:
|
|
|
232
232
|
The `force` option can drop an existing table.
|
|
233
233
|
|
|
234
234
|
```ruby
|
|
235
|
-
class CreateUsers < ActiveRecord::Migration[
|
|
235
|
+
class CreateUsers < ActiveRecord::Migration[8.0]
|
|
236
236
|
def change
|
|
237
237
|
create_table :users, force: true do |t|
|
|
238
238
|
# ...
|
|
@@ -246,7 +246,7 @@ end
|
|
|
246
246
|
Create tables without the `force` option.
|
|
247
247
|
|
|
248
248
|
```ruby
|
|
249
|
-
class CreateUsers < ActiveRecord::Migration[
|
|
249
|
+
class CreateUsers < ActiveRecord::Migration[8.0]
|
|
250
250
|
def change
|
|
251
251
|
create_table :users do |t|
|
|
252
252
|
# ...
|
|
@@ -264,7 +264,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
|
264
264
|
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.
|
|
265
265
|
|
|
266
266
|
```ruby
|
|
267
|
-
class AddIdToCitiesUsers < ActiveRecord::Migration[
|
|
267
|
+
class AddIdToCitiesUsers < ActiveRecord::Migration[8.0]
|
|
268
268
|
def change
|
|
269
269
|
add_column :cities_users, :id, :primary_key
|
|
270
270
|
end
|
|
@@ -284,7 +284,7 @@ Create a new table and migrate the data with the same steps as [renaming a table
|
|
|
284
284
|
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.
|
|
285
285
|
|
|
286
286
|
```ruby
|
|
287
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
|
287
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
|
|
288
288
|
def change
|
|
289
289
|
add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
|
|
290
290
|
end
|
|
@@ -304,7 +304,7 @@ Add a non-generated column and use callbacks or triggers instead (or a virtual g
|
|
|
304
304
|
Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
|
|
305
305
|
|
|
306
306
|
```ruby
|
|
307
|
-
class AddCheckConstraint < ActiveRecord::Migration[
|
|
307
|
+
class AddCheckConstraint < ActiveRecord::Migration[8.0]
|
|
308
308
|
def change
|
|
309
309
|
add_check_constraint :users, "price > 0", name: "price_check"
|
|
310
310
|
end
|
|
@@ -316,7 +316,7 @@ end
|
|
|
316
316
|
Add the check constraint without validating existing rows:
|
|
317
317
|
|
|
318
318
|
```ruby
|
|
319
|
-
class AddCheckConstraint < ActiveRecord::Migration[
|
|
319
|
+
class AddCheckConstraint < ActiveRecord::Migration[8.0]
|
|
320
320
|
def change
|
|
321
321
|
add_check_constraint :users, "price > 0", name: "price_check", validate: false
|
|
322
322
|
end
|
|
@@ -326,7 +326,7 @@ end
|
|
|
326
326
|
Then validate them in a separate migration.
|
|
327
327
|
|
|
328
328
|
```ruby
|
|
329
|
-
class ValidateCheckConstraint < ActiveRecord::Migration[
|
|
329
|
+
class ValidateCheckConstraint < ActiveRecord::Migration[8.0]
|
|
330
330
|
def change
|
|
331
331
|
validate_check_constraint :users, name: "price_check"
|
|
332
332
|
end
|
|
@@ -342,7 +342,7 @@ end
|
|
|
342
342
|
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
|
343
343
|
|
|
344
344
|
```ruby
|
|
345
|
-
class ExecuteSQL < ActiveRecord::Migration[
|
|
345
|
+
class ExecuteSQL < ActiveRecord::Migration[8.0]
|
|
346
346
|
def change
|
|
347
347
|
safety_assured { execute "..." }
|
|
348
348
|
end
|
|
@@ -351,12 +351,14 @@ end
|
|
|
351
351
|
|
|
352
352
|
### Backfilling data
|
|
353
353
|
|
|
354
|
+
Note: Strong Migrations does not detect dangerous backfills.
|
|
355
|
+
|
|
354
356
|
#### Bad
|
|
355
357
|
|
|
356
358
|
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/).
|
|
357
359
|
|
|
358
360
|
```ruby
|
|
359
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
|
361
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
|
|
360
362
|
def change
|
|
361
363
|
add_column :users, :some_column, :text
|
|
362
364
|
User.update_all some_column: "default_value"
|
|
@@ -371,18 +373,20 @@ Also, running a single query to update data can cause issues for large tables.
|
|
|
371
373
|
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!`.
|
|
372
374
|
|
|
373
375
|
```ruby
|
|
374
|
-
class BackfillSomeColumn < ActiveRecord::Migration[
|
|
376
|
+
class BackfillSomeColumn < ActiveRecord::Migration[8.0]
|
|
375
377
|
disable_ddl_transaction!
|
|
376
378
|
|
|
377
379
|
def up
|
|
378
|
-
User.unscoped.in_batches do |relation|
|
|
379
|
-
relation.update_all some_column: "default_value"
|
|
380
|
+
User.unscoped.in_batches(of: 10000) do |relation|
|
|
381
|
+
relation.where(some_column: nil).update_all some_column: "default_value"
|
|
380
382
|
sleep(0.01) # throttle
|
|
381
383
|
end
|
|
382
384
|
end
|
|
383
385
|
end
|
|
384
386
|
```
|
|
385
387
|
|
|
388
|
+
Note: If backfilling with a method other than `update_all`, use `User.reset_column_information` to ensure the model has up-to-date column information.
|
|
389
|
+
|
|
386
390
|
### Adding an index non-concurrently
|
|
387
391
|
|
|
388
392
|
:turtle: Safe by default available
|
|
@@ -392,7 +396,7 @@ end
|
|
|
392
396
|
In Postgres, adding an index non-concurrently blocks writes.
|
|
393
397
|
|
|
394
398
|
```ruby
|
|
395
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
|
399
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
|
|
396
400
|
def change
|
|
397
401
|
add_index :users, :some_column
|
|
398
402
|
end
|
|
@@ -404,7 +408,7 @@ end
|
|
|
404
408
|
Add indexes concurrently.
|
|
405
409
|
|
|
406
410
|
```ruby
|
|
407
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
|
411
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
|
|
408
412
|
disable_ddl_transaction!
|
|
409
413
|
|
|
410
414
|
def change
|
|
@@ -430,7 +434,7 @@ rails g index table column
|
|
|
430
434
|
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
|
431
435
|
|
|
432
436
|
```ruby
|
|
433
|
-
class AddReferenceToUsers < ActiveRecord::Migration[
|
|
437
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.0]
|
|
434
438
|
def change
|
|
435
439
|
add_reference :users, :city
|
|
436
440
|
end
|
|
@@ -442,7 +446,7 @@ end
|
|
|
442
446
|
Make sure the index is added concurrently.
|
|
443
447
|
|
|
444
448
|
```ruby
|
|
445
|
-
class AddReferenceToUsers < ActiveRecord::Migration[
|
|
449
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.0]
|
|
446
450
|
disable_ddl_transaction!
|
|
447
451
|
|
|
448
452
|
def change
|
|
@@ -460,7 +464,7 @@ end
|
|
|
460
464
|
In Postgres, adding a foreign key blocks writes on both tables.
|
|
461
465
|
|
|
462
466
|
```ruby
|
|
463
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[
|
|
467
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.0]
|
|
464
468
|
def change
|
|
465
469
|
add_foreign_key :users, :orders
|
|
466
470
|
end
|
|
@@ -470,7 +474,7 @@ end
|
|
|
470
474
|
or
|
|
471
475
|
|
|
472
476
|
```ruby
|
|
473
|
-
class AddReferenceToUsers < ActiveRecord::Migration[
|
|
477
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.0]
|
|
474
478
|
def change
|
|
475
479
|
add_reference :users, :order, foreign_key: true
|
|
476
480
|
end
|
|
@@ -482,7 +486,7 @@ end
|
|
|
482
486
|
Add the foreign key without validating existing rows:
|
|
483
487
|
|
|
484
488
|
```ruby
|
|
485
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[
|
|
489
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.0]
|
|
486
490
|
def change
|
|
487
491
|
add_foreign_key :users, :orders, validate: false
|
|
488
492
|
end
|
|
@@ -492,7 +496,7 @@ end
|
|
|
492
496
|
Then validate them in a separate migration.
|
|
493
497
|
|
|
494
498
|
```ruby
|
|
495
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[
|
|
499
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.0]
|
|
496
500
|
def change
|
|
497
501
|
validate_foreign_key :users, :orders
|
|
498
502
|
end
|
|
@@ -506,7 +510,7 @@ end
|
|
|
506
510
|
In Postgres, adding a unique constraint creates a unique index, which blocks reads and writes.
|
|
507
511
|
|
|
508
512
|
```ruby
|
|
509
|
-
class
|
|
513
|
+
class AddUniqueConstraint < ActiveRecord::Migration[8.0]
|
|
510
514
|
def change
|
|
511
515
|
add_unique_constraint :users, :some_column
|
|
512
516
|
end
|
|
@@ -518,7 +522,7 @@ end
|
|
|
518
522
|
Create a unique index concurrently, then use it for the constraint.
|
|
519
523
|
|
|
520
524
|
```ruby
|
|
521
|
-
class
|
|
525
|
+
class AddUniqueConstraint < ActiveRecord::Migration[8.0]
|
|
522
526
|
disable_ddl_transaction!
|
|
523
527
|
|
|
524
528
|
def up
|
|
@@ -539,7 +543,7 @@ end
|
|
|
539
543
|
In Postgres, adding an exclusion constraint blocks reads and writes while every row is checked.
|
|
540
544
|
|
|
541
545
|
```ruby
|
|
542
|
-
class
|
|
546
|
+
class AddExclusionConstraint < ActiveRecord::Migration[8.0]
|
|
543
547
|
def change
|
|
544
548
|
add_exclusion_constraint :users, "number WITH =", using: :gist
|
|
545
549
|
end
|
|
@@ -557,7 +561,7 @@ end
|
|
|
557
561
|
In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
|
558
562
|
|
|
559
563
|
```ruby
|
|
560
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[
|
|
564
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[8.0]
|
|
561
565
|
def change
|
|
562
566
|
add_column :users, :properties, :json
|
|
563
567
|
end
|
|
@@ -569,7 +573,7 @@ end
|
|
|
569
573
|
Use `jsonb` instead.
|
|
570
574
|
|
|
571
575
|
```ruby
|
|
572
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[
|
|
576
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[8.0]
|
|
573
577
|
def change
|
|
574
578
|
add_column :users, :properties, :jsonb
|
|
575
579
|
end
|
|
@@ -585,7 +589,7 @@ end
|
|
|
585
589
|
In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
|
586
590
|
|
|
587
591
|
```ruby
|
|
588
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[
|
|
592
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[8.0]
|
|
589
593
|
def change
|
|
590
594
|
change_column_null :users, :some_column, false
|
|
591
595
|
end
|
|
@@ -596,87 +600,54 @@ end
|
|
|
596
600
|
|
|
597
601
|
Instead, add a check constraint.
|
|
598
602
|
|
|
599
|
-
For Rails 6.1+, use:
|
|
600
|
-
|
|
601
603
|
```ruby
|
|
602
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[
|
|
604
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[8.0]
|
|
603
605
|
def change
|
|
604
606
|
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
|
605
607
|
end
|
|
606
608
|
end
|
|
607
609
|
```
|
|
608
610
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
```ruby
|
|
612
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
|
613
|
-
def change
|
|
614
|
-
safety_assured do
|
|
615
|
-
execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
|
|
616
|
-
end
|
|
617
|
-
end
|
|
618
|
-
end
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
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.
|
|
622
|
-
|
|
623
|
-
For Rails 6.1+, use:
|
|
611
|
+
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.
|
|
624
612
|
|
|
625
613
|
```ruby
|
|
626
|
-
class ValidateSomeColumnNotNull < ActiveRecord::Migration[
|
|
627
|
-
def
|
|
614
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[8.0]
|
|
615
|
+
def up
|
|
628
616
|
validate_check_constraint :users, name: "users_some_column_null"
|
|
629
|
-
|
|
630
|
-
# in Postgres 12+, you can then safely set NOT NULL on the column
|
|
631
617
|
change_column_null :users, :some_column, false
|
|
632
618
|
remove_check_constraint :users, name: "users_some_column_null"
|
|
633
619
|
end
|
|
634
|
-
end
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
For Rails < 6.1, use:
|
|
638
|
-
|
|
639
|
-
```ruby
|
|
640
|
-
class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
|
641
|
-
def change
|
|
642
|
-
safety_assured do
|
|
643
|
-
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
|
|
644
|
-
end
|
|
645
620
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
|
|
650
|
-
end
|
|
621
|
+
def down
|
|
622
|
+
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
|
623
|
+
change_column_null :users, :some_column, true
|
|
651
624
|
end
|
|
652
625
|
end
|
|
653
626
|
```
|
|
654
627
|
|
|
655
|
-
### Adding a column with a default value
|
|
628
|
+
### Adding a column with a volatile default value
|
|
656
629
|
|
|
657
630
|
#### Bad
|
|
658
631
|
|
|
659
|
-
|
|
632
|
+
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.
|
|
660
633
|
|
|
661
634
|
```ruby
|
|
662
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
|
635
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
|
|
663
636
|
def change
|
|
664
|
-
add_column :users, :some_column, :
|
|
637
|
+
add_column :users, :some_column, :uuid, default: "gen_random_uuid()"
|
|
665
638
|
end
|
|
666
639
|
end
|
|
667
640
|
```
|
|
668
641
|
|
|
669
|
-
In Postgres 11+, this no longer requires a table rewrite and is safe (except for volatile functions like `gen_random_uuid()`).
|
|
670
|
-
|
|
671
642
|
#### Good
|
|
672
643
|
|
|
673
644
|
Instead, add the column without a default value, then change the default.
|
|
674
645
|
|
|
675
646
|
```ruby
|
|
676
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
|
647
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
|
|
677
648
|
def up
|
|
678
|
-
add_column :users, :some_column, :
|
|
679
|
-
change_column_default :users, :some_column, "
|
|
649
|
+
add_column :users, :some_column, :uuid
|
|
650
|
+
change_column_default :users, :some_column, from: nil, to: "gen_random_uuid()"
|
|
680
651
|
end
|
|
681
652
|
|
|
682
653
|
def down
|
|
@@ -724,7 +695,7 @@ config.active_record.partial_inserts = false
|
|
|
724
695
|
Adding a non-unique index with more than three columns rarely improves performance.
|
|
725
696
|
|
|
726
697
|
```ruby
|
|
727
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
|
698
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
|
|
728
699
|
def change
|
|
729
700
|
add_index :users, [:a, :b, :c, :d]
|
|
730
701
|
end
|
|
@@ -736,9 +707,9 @@ end
|
|
|
736
707
|
Instead, start an index with columns that narrow down the results the most.
|
|
737
708
|
|
|
738
709
|
```ruby
|
|
739
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
|
710
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
|
|
740
711
|
def change
|
|
741
|
-
add_index :users, [:
|
|
712
|
+
add_index :users, [:d, :b]
|
|
742
713
|
end
|
|
743
714
|
end
|
|
744
715
|
```
|
|
@@ -750,7 +721,7 @@ For Postgres, be sure to add them concurrently.
|
|
|
750
721
|
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.
|
|
751
722
|
|
|
752
723
|
```ruby
|
|
753
|
-
class MySafeMigration < ActiveRecord::Migration[
|
|
724
|
+
class MySafeMigration < ActiveRecord::Migration[8.0]
|
|
754
725
|
def change
|
|
755
726
|
safety_assured { remove_column :users, :some_column }
|
|
756
727
|
end
|
|
@@ -761,7 +732,7 @@ Certain methods like `execute` and `change_table` cannot be inspected and are pr
|
|
|
761
732
|
|
|
762
733
|
## Safe by Default
|
|
763
734
|
|
|
764
|
-
Make operations safe by default.
|
|
735
|
+
Make certain operations safe by default. This allows you to write the code under the "Bad" section, but the migration will be performed as if you had written the "Good" version.
|
|
765
736
|
|
|
766
737
|
- adding and removing an index
|
|
767
738
|
- adding a foreign key
|
|
@@ -810,6 +781,16 @@ StrongMigrations.disable_check(:add_index)
|
|
|
810
781
|
|
|
811
782
|
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
|
|
812
783
|
|
|
784
|
+
## Skip Databases
|
|
785
|
+
|
|
786
|
+
Skip checks and other functionality for specific databases with:
|
|
787
|
+
|
|
788
|
+
```ruby
|
|
789
|
+
StrongMigrations.skip_database(:catalog)
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
Note: This does not affect `alphabetize_schema`.
|
|
793
|
+
|
|
813
794
|
## Down Migrations / Rollbacks
|
|
814
795
|
|
|
815
796
|
By default, checks are disabled when migrating down. Enable them with:
|
|
@@ -891,7 +872,21 @@ production:
|
|
|
891
872
|
|
|
892
873
|
For HTTP connections, Redis, and other services, check out [this guide](https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts).
|
|
893
874
|
|
|
894
|
-
##
|
|
875
|
+
## Invalid Indexes
|
|
876
|
+
|
|
877
|
+
In Postgres, adding an index non-concurrently can leave behind an invalid index if the lock timeout is reached. Running the migration again can result in an error.
|
|
878
|
+
|
|
879
|
+
To automatically remove the invalid index when the migration runs again, use:
|
|
880
|
+
|
|
881
|
+
```ruby
|
|
882
|
+
StrongMigrations.remove_invalid_indexes = true
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
Note: This feature is experimental.
|
|
886
|
+
|
|
887
|
+
## Lock Timeout Retries
|
|
888
|
+
|
|
889
|
+
Note: This feature is experimental.
|
|
895
890
|
|
|
896
891
|
There’s the option to automatically retry statements for migrations when the lock timeout is reached. Here’s how it works:
|
|
897
892
|
|
|
@@ -915,7 +910,7 @@ StrongMigrations.lock_timeout_retry_delay = 10.seconds
|
|
|
915
910
|
To mark migrations as safe that were created before installing this gem, create an initializer with:
|
|
916
911
|
|
|
917
912
|
```ruby
|
|
918
|
-
StrongMigrations.start_after =
|
|
913
|
+
StrongMigrations.start_after = 20250101000000
|
|
919
914
|
```
|
|
920
915
|
|
|
921
916
|
Use the version from your latest migration.
|
|
@@ -925,14 +920,14 @@ Use the version from your latest migration.
|
|
|
925
920
|
If your development database version is different from production, you can specify the production version so the right checks run in development.
|
|
926
921
|
|
|
927
922
|
```ruby
|
|
928
|
-
StrongMigrations.target_version = 10 # or
|
|
923
|
+
StrongMigrations.target_version = 10 # or 8.0, 10.5, etc
|
|
929
924
|
```
|
|
930
925
|
|
|
931
|
-
The major version works well for Postgres, while the
|
|
926
|
+
The major version works well for Postgres, while the major and minor version is recommended for MySQL and MariaDB.
|
|
932
927
|
|
|
933
928
|
For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
|
|
934
929
|
|
|
935
|
-
If your app has multiple databases with different versions,
|
|
930
|
+
If your app has multiple databases with different versions, you can use:
|
|
936
931
|
|
|
937
932
|
```ruby
|
|
938
933
|
StrongMigrations.target_version = {primary: 13, catalog: 15}
|
|
@@ -972,15 +967,18 @@ You probably don’t need this gem for smaller projects, as operations that are
|
|
|
972
967
|
|
|
973
968
|
## Additional Reading
|
|
974
969
|
|
|
975
|
-
- [Rails Migrations with No Downtime](https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
|
|
976
970
|
- [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)
|
|
977
|
-
- [
|
|
971
|
+
- [MySQL InnoDB Online DDL Operations](https://dev.mysql.com/doc/refman/en/innodb-online-ddl-operations.html)
|
|
978
972
|
- [MariaDB InnoDB Online DDL Overview](https://mariadb.com/kb/en/innodb-online-ddl-overview/)
|
|
979
973
|
|
|
980
974
|
## Credits
|
|
981
975
|
|
|
982
976
|
Thanks to Bob Remeika and David Waller for the [original code](https://github.com/foobarfighter/safe-migrations) and [Sean Huber](https://github.com/LendingHome/zero_downtime_migrations) for the bad/good readme format.
|
|
983
977
|
|
|
978
|
+
## History
|
|
979
|
+
|
|
980
|
+
View the [changelog](https://github.com/ankane/strong_migrations/blob/master/CHANGELOG.md)
|
|
981
|
+
|
|
984
982
|
## Contributing
|
|
985
983
|
|
|
986
984
|
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
|
@@ -21,21 +21,17 @@ module StrongMigrations
|
|
|
21
21
|
|
|
22
22
|
def target_version
|
|
23
23
|
case adapter
|
|
24
|
-
when /mysql/
|
|
24
|
+
when /mysql|trilogy/
|
|
25
25
|
# could try to connect to database and check for MariaDB
|
|
26
26
|
# but this should be fine
|
|
27
|
-
|
|
27
|
+
"8.0"
|
|
28
28
|
else
|
|
29
29
|
"10"
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def adapter
|
|
34
|
-
|
|
35
|
-
ActiveRecord::Base.connection_db_config.adapter.to_s
|
|
36
|
-
else
|
|
37
|
-
ActiveRecord::Base.connection_config[:adapter].to_s
|
|
38
|
-
end
|
|
34
|
+
ActiveRecord::Base.connection_db_config.adapter.to_s
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
def postgresql?
|
|
@@ -13,11 +13,11 @@ module StrongMigrations
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def set_statement_timeout(timeout)
|
|
16
|
-
|
|
16
|
+
# do nothing
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def set_lock_timeout(timeout)
|
|
20
|
-
|
|
20
|
+
# do nothing
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def check_lock_timeout(limit)
|
|
@@ -40,6 +40,9 @@ module StrongMigrations
|
|
|
40
40
|
["primary_key"]
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def max_constraint_name_length
|
|
44
|
+
end
|
|
45
|
+
|
|
43
46
|
private
|
|
44
47
|
|
|
45
48
|
def connection
|
|
@@ -55,14 +58,6 @@ module StrongMigrations
|
|
|
55
58
|
version =
|
|
56
59
|
if target_version && StrongMigrations.developer_env?
|
|
57
60
|
if target_version.is_a?(Hash)
|
|
58
|
-
# Active Record 6.0 supports multiple databases
|
|
59
|
-
# but connection.pool.spec.name always returns "primary"
|
|
60
|
-
# in migrations with rails db:migrate
|
|
61
|
-
if ActiveRecord::VERSION::STRING.to_f < 6.1
|
|
62
|
-
# error class is not shown in db:migrate output so ensure message is descriptive
|
|
63
|
-
raise StrongMigrations::Error, "StrongMigrations.target_version does not support multiple databases for Active Record < 6.1"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
61
|
db_config_name = connection.pool.db_config.name
|
|
67
62
|
target_version.stringify_keys.fetch(db_config_name) do
|
|
68
63
|
# error class is not shown in db:migrate output so ensure message is descriptive
|