strong_migrations 2.6.0 → 2.7.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 +5 -0
- data/README.md +149 -86
- data/lib/strong_migrations/checks.rb +30 -8
- data/lib/strong_migrations/error_messages.rb +39 -2
- data/lib/strong_migrations/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 250e255ba679a0e581fb86ed13b2824cf5749b7dc3ac5bf0a26bfffebe314fb8
|
|
4
|
+
data.tar.gz: d91882cb145d2f59a688e41e0d20301c6ca41d3e5e9790be9996eb256cc26dd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0bba6d87fd23e3bf68d8a8274cc6d22346a73f0001e8078f99f3c7a039772e39ded14138a2ad87eb7476f855a56647a1e07f84e7dadc96d5f3df53aebd27c9a
|
|
7
|
+
data.tar.gz: b985a53c42f12ec3268f6fd2570bdb2cdf777f80217b17a68872e86d4366b5939a702c0b8551b0ddab4f44ddfb9ef5da8a3f74ca97365124a086d46c2fe66f10
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -66,6 +66,7 @@ Potentially dangerous operations:
|
|
|
66
66
|
- [creating a table with the force option](#creating-a-table-with-the-force-option)
|
|
67
67
|
- [adding an auto-incrementing column](#adding-an-auto-incrementing-column)
|
|
68
68
|
- [adding a stored generated column](#adding-a-stored-generated-column)
|
|
69
|
+
- [adding a foreign key](#adding-a-foreign-key)
|
|
69
70
|
- [adding a check constraint](#adding-a-check-constraint)
|
|
70
71
|
- [executing SQL directly](#executing-SQL-directly)
|
|
71
72
|
- [backfilling data](#backfilling-data)
|
|
@@ -74,18 +75,18 @@ Postgres-specific checks:
|
|
|
74
75
|
|
|
75
76
|
- [adding an index non-concurrently](#adding-an-index-non-concurrently)
|
|
76
77
|
- [adding a reference](#adding-a-reference)
|
|
77
|
-
- [adding a foreign key](#adding-a-foreign-key)
|
|
78
78
|
- [adding a unique constraint](#adding-a-unique-constraint)
|
|
79
79
|
- [adding an exclusion constraint](#adding-an-exclusion-constraint)
|
|
80
80
|
- [adding a json column](#adding-a-json-column)
|
|
81
|
-
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
|
82
81
|
- [adding a column with a volatile default value](#adding-a-column-with-a-volatile-default-value)
|
|
82
|
+
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
|
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
88
|
- [using shared or exclusive locking](#using-shared-or-exclusive-locking)
|
|
89
|
+
- [adding a column with an expression default value](#adding-a-column-with-an-expression-default-value)
|
|
89
90
|
|
|
90
91
|
Best practices:
|
|
91
92
|
|
|
@@ -297,9 +298,80 @@ end
|
|
|
297
298
|
|
|
298
299
|
Add a non-generated column and use callbacks or triggers instead (or a virtual generated column with MySQL and MariaDB).
|
|
299
300
|
|
|
301
|
+
### Adding a foreign key
|
|
302
|
+
|
|
303
|
+
:turtle: Safe by default available for Postgres
|
|
304
|
+
|
|
305
|
+
#### Bad
|
|
306
|
+
|
|
307
|
+
Adding a foreign key blocks writes on both tables.
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
311
|
+
def change
|
|
312
|
+
add_foreign_key :users, :orders
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
or
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.1]
|
|
321
|
+
def change
|
|
322
|
+
add_reference :users, :order, foreign_key: true
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### Good - Postgres
|
|
328
|
+
|
|
329
|
+
Add the foreign key without validating existing rows:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
333
|
+
def change
|
|
334
|
+
add_foreign_key :users, :orders, validate: false
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Then validate them in a separate migration.
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
343
|
+
def change
|
|
344
|
+
validate_foreign_key :users, :orders
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### Good - MySQL and MariaDB
|
|
350
|
+
|
|
351
|
+
If you are 100% sure all rows are valid and migrations do not use a connection pooler, you can add the foreign key without validating existing rows:
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
355
|
+
def up
|
|
356
|
+
safety_assured do
|
|
357
|
+
begin
|
|
358
|
+
execute "SET SESSION foreign_key_checks = 0"
|
|
359
|
+
add_foreign_key :users, :orders
|
|
360
|
+
ensure
|
|
361
|
+
execute "SET SESSION foreign_key_checks = 1"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def down
|
|
367
|
+
remove_foreign_key :users, :orders
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
```
|
|
371
|
+
|
|
300
372
|
### Adding a check constraint
|
|
301
373
|
|
|
302
|
-
:turtle: Safe by default available
|
|
374
|
+
:turtle: Safe by default available for Postgres
|
|
303
375
|
|
|
304
376
|
#### Bad
|
|
305
377
|
|
|
@@ -389,6 +461,8 @@ end
|
|
|
389
461
|
|
|
390
462
|
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.
|
|
391
463
|
|
|
464
|
+
## Postgres Checks
|
|
465
|
+
|
|
392
466
|
### Adding an index non-concurrently
|
|
393
467
|
|
|
394
468
|
:turtle: Safe by default available
|
|
@@ -457,54 +531,6 @@ class AddReferenceToUsers < ActiveRecord::Migration[8.1]
|
|
|
457
531
|
end
|
|
458
532
|
```
|
|
459
533
|
|
|
460
|
-
### Adding a foreign key
|
|
461
|
-
|
|
462
|
-
:turtle: Safe by default available
|
|
463
|
-
|
|
464
|
-
#### Bad
|
|
465
|
-
|
|
466
|
-
In Postgres, adding a foreign key blocks writes on both tables.
|
|
467
|
-
|
|
468
|
-
```ruby
|
|
469
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
470
|
-
def change
|
|
471
|
-
add_foreign_key :users, :orders
|
|
472
|
-
end
|
|
473
|
-
end
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
or
|
|
477
|
-
|
|
478
|
-
```ruby
|
|
479
|
-
class AddReferenceToUsers < ActiveRecord::Migration[8.1]
|
|
480
|
-
def change
|
|
481
|
-
add_reference :users, :order, foreign_key: true
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
#### Good
|
|
487
|
-
|
|
488
|
-
Add the foreign key without validating existing rows:
|
|
489
|
-
|
|
490
|
-
```ruby
|
|
491
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
492
|
-
def change
|
|
493
|
-
add_foreign_key :users, :orders, validate: false
|
|
494
|
-
end
|
|
495
|
-
end
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
Then validate them in a separate migration.
|
|
499
|
-
|
|
500
|
-
```ruby
|
|
501
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
502
|
-
def change
|
|
503
|
-
validate_foreign_key :users, :orders
|
|
504
|
-
end
|
|
505
|
-
end
|
|
506
|
-
```
|
|
507
|
-
|
|
508
534
|
### Adding a unique constraint
|
|
509
535
|
|
|
510
536
|
#### Bad
|
|
@@ -582,6 +608,39 @@ class AddPropertiesToUsers < ActiveRecord::Migration[8.1]
|
|
|
582
608
|
end
|
|
583
609
|
```
|
|
584
610
|
|
|
611
|
+
### Adding a column with a volatile default value
|
|
612
|
+
|
|
613
|
+
#### Bad
|
|
614
|
+
|
|
615
|
+
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.
|
|
616
|
+
|
|
617
|
+
```ruby
|
|
618
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
619
|
+
def change
|
|
620
|
+
add_column :users, :some_column, :uuid, default: "gen_random_uuid()"
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
#### Good
|
|
626
|
+
|
|
627
|
+
Instead, add the column without a default value, then change the default.
|
|
628
|
+
|
|
629
|
+
```ruby
|
|
630
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
631
|
+
def up
|
|
632
|
+
add_column :users, :some_column, :uuid
|
|
633
|
+
change_column_default :users, :some_column, "gen_random_uuid()"
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def down
|
|
637
|
+
remove_column :users, :some_column
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Then [backfill the data](#backfilling-data).
|
|
643
|
+
|
|
585
644
|
### Setting NOT NULL on an existing column
|
|
586
645
|
|
|
587
646
|
:turtle: Safe by default available
|
|
@@ -627,39 +686,6 @@ class ValidateSomeColumnNotNull < ActiveRecord::Migration[8.1]
|
|
|
627
686
|
end
|
|
628
687
|
```
|
|
629
688
|
|
|
630
|
-
### Adding a column with a volatile default value
|
|
631
|
-
|
|
632
|
-
#### Bad
|
|
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.
|
|
635
|
-
|
|
636
|
-
```ruby
|
|
637
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
638
|
-
def change
|
|
639
|
-
add_column :users, :some_column, :uuid, default: "gen_random_uuid()"
|
|
640
|
-
end
|
|
641
|
-
end
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
#### Good
|
|
645
|
-
|
|
646
|
-
Instead, add the column without a default value, then change the default.
|
|
647
|
-
|
|
648
|
-
```ruby
|
|
649
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
650
|
-
def up
|
|
651
|
-
add_column :users, :some_column, :uuid
|
|
652
|
-
change_column_default :users, :some_column, from: nil, to: "gen_random_uuid()"
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
def down
|
|
656
|
-
remove_column :users, :some_column
|
|
657
|
-
end
|
|
658
|
-
end
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
Then [backfill the data](#backfilling-data).
|
|
662
|
-
|
|
663
689
|
### Renaming a schema
|
|
664
690
|
|
|
665
691
|
#### Bad
|
|
@@ -685,6 +711,8 @@ A safer approach is to:
|
|
|
685
711
|
5. Stop writing to the old schema
|
|
686
712
|
6. Drop the old schema
|
|
687
713
|
|
|
714
|
+
## MySQL and MariaDB Checks
|
|
715
|
+
|
|
688
716
|
### Using the COPY algorithm
|
|
689
717
|
|
|
690
718
|
#### Bad
|
|
@@ -737,6 +765,41 @@ class AddSomeIndexToUsers < ActiveRecord::Migration[8.2]
|
|
|
737
765
|
end
|
|
738
766
|
```
|
|
739
767
|
|
|
768
|
+
### Adding a column with an expression default value
|
|
769
|
+
|
|
770
|
+
#### Bad
|
|
771
|
+
|
|
772
|
+
In MySQL and MariaDB, adding a column with an expression default value to an existing table causes the entire table to be rewritten. During this time, writes are blocked.
|
|
773
|
+
|
|
774
|
+
```ruby
|
|
775
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
776
|
+
def change
|
|
777
|
+
add_column :users, :some_column, :datetime, default: -> { "(now())" }
|
|
778
|
+
end
|
|
779
|
+
end
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
#### Good
|
|
783
|
+
|
|
784
|
+
Instead, add the column without a default value, then change the default.
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
788
|
+
def up
|
|
789
|
+
add_column :users, :some_column, :datetime
|
|
790
|
+
change_column_default :users, :some_column, -> { "(now())" }
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def down
|
|
794
|
+
remove_column :users, :some_column
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Then [backfill the data](#backfilling-data).
|
|
800
|
+
|
|
801
|
+
## Best Practices
|
|
802
|
+
|
|
740
803
|
### Keeping non-unique indexes to three columns or less
|
|
741
804
|
|
|
742
805
|
#### Bad
|
|
@@ -876,7 +939,7 @@ ALTER ROLE myuser SET lock_timeout = '10s';
|
|
|
876
939
|
ALTER ROLE myuser SET statement_timeout = '1h';
|
|
877
940
|
```
|
|
878
941
|
|
|
879
|
-
Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
|
|
942
|
+
Note: If you use a connection pooler like PgBouncer in transaction mode, you must set timeouts on the database user.
|
|
880
943
|
|
|
881
944
|
## App Timeouts
|
|
882
945
|
|
|
@@ -892,7 +955,7 @@ production:
|
|
|
892
955
|
lock_timeout: 10s
|
|
893
956
|
```
|
|
894
957
|
|
|
895
|
-
Note: If you use PgBouncer in transaction mode, you must set the statement and lock timeouts on the database user as shown above.
|
|
958
|
+
Note: If you use a connection pooler like PgBouncer in transaction mode, you must set the statement and lock timeouts on the database user as shown above.
|
|
896
959
|
|
|
897
960
|
For MySQL:
|
|
898
961
|
|
|
@@ -35,9 +35,18 @@ module StrongMigrations
|
|
|
35
35
|
# keep track of new columns of change_column_default check
|
|
36
36
|
@new_columns << [table.to_s, column.to_s]
|
|
37
37
|
|
|
38
|
-
#
|
|
38
|
+
# adding a column with a volatile default is not safe with Postgres
|
|
39
|
+
# https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
|
|
40
|
+
# functions like random() and clock_timestamp() are volatile
|
|
41
|
+
# functions like concat('A', 'B') are safe
|
|
42
|
+
# default expressions in Postgres cannot reference other columns
|
|
39
43
|
#
|
|
40
|
-
#
|
|
44
|
+
# adding a column with an expression default is not safe with MySQL
|
|
45
|
+
# even constant expressions like (3) are not safe
|
|
46
|
+
# literals like 3 are safe
|
|
47
|
+
#
|
|
48
|
+
# Active Record quotes default values except for procs
|
|
49
|
+
# there is also a special case for uuid columns
|
|
41
50
|
# https://github.com/rails/rails/blob/v7.0.3.1/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L92-L93
|
|
42
51
|
if !default.nil? && (!adapter.add_column_default_safe? || (volatile = (postgresql? && type.to_s == "uuid" && default.to_s.include?("()") && adapter.default_volatile?(default))))
|
|
43
52
|
if options[:null] == false
|
|
@@ -52,13 +61,14 @@ module StrongMigrations
|
|
|
52
61
|
code: backfill_code(table, column, default, volatile),
|
|
53
62
|
append: append,
|
|
54
63
|
rewrite_blocks: adapter.rewrite_blocks,
|
|
55
|
-
default_type:
|
|
56
|
-
elsif default.is_a?(Proc)
|
|
57
|
-
# adding a column with a VOLATILE default is not safe
|
|
58
|
-
# https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
|
|
59
|
-
# functions like random() and clock_timestamp() are VOLATILE
|
|
64
|
+
default_type: volatile ? "volatile" : "non-null"
|
|
65
|
+
elsif default.is_a?(Proc)
|
|
60
66
|
# check for Proc to match Active Record
|
|
61
|
-
raise_error :add_column_default_callable
|
|
67
|
+
raise_error :add_column_default_callable,
|
|
68
|
+
add_command: command_str("add_column", [table, column, type, options.except(:default)]),
|
|
69
|
+
change_command: command_str("change_column_default", [table, column]),
|
|
70
|
+
remove_command: command_str("remove_column", [table, column]),
|
|
71
|
+
default_type: postgresql? ? "volatile" : "an expression"
|
|
62
72
|
end
|
|
63
73
|
|
|
64
74
|
if type.to_s == "json" && postgresql?
|
|
@@ -117,6 +127,11 @@ module StrongMigrations
|
|
|
117
127
|
raise_error :add_foreign_key,
|
|
118
128
|
add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
|
|
119
129
|
validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
|
|
130
|
+
elsif mysql? || mariadb?
|
|
131
|
+
raise_error :add_foreign_key_mysql,
|
|
132
|
+
add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options]),
|
|
133
|
+
# TODO exclude some options?
|
|
134
|
+
remove_foreign_key_code: command_str("remove_foreign_key", [from_table, to_table, options])
|
|
120
135
|
end
|
|
121
136
|
end
|
|
122
137
|
|
|
@@ -187,6 +202,13 @@ module StrongMigrations
|
|
|
187
202
|
command: command_str(method, [table, reference, options]),
|
|
188
203
|
append: append
|
|
189
204
|
end
|
|
205
|
+
elsif mysql? || mariadb?
|
|
206
|
+
if options[:foreign_key]
|
|
207
|
+
raise_error :add_reference,
|
|
208
|
+
headline: "Adding a foreign key blocks writes on both tables.",
|
|
209
|
+
command: command_str(method, [table, reference, options.except(:foreign_key)]),
|
|
210
|
+
append: "\n\nThen add the foreign key in a separate migration."
|
|
211
|
+
end
|
|
190
212
|
end
|
|
191
213
|
|
|
192
214
|
check_algorithm_option("add_reference", *args, **options)
|
|
@@ -27,8 +27,23 @@ end",
|
|
|
27
27
|
|
|
28
28
|
add_column_default_callable:
|
|
29
29
|
"Strong Migrations does not support inspecting callable default values.
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
If the default value is %{default_type}, add the column without a default value, then change the default.
|
|
32
|
+
|
|
33
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
34
|
+
def up
|
|
35
|
+
%{add_command}
|
|
36
|
+
%{change_command}, -> { ... }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def down
|
|
40
|
+
%{remove_command}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Then backfill the existing rows in the Rails console or a separate migration with disable_ddl_transaction!.
|
|
45
|
+
|
|
46
|
+
Otherwise, wrap this step in a safety_assured { ... } block.",
|
|
32
47
|
|
|
33
48
|
add_column_json:
|
|
34
49
|
"There's no equality operator for the json column type, which can cause errors for
|
|
@@ -235,6 +250,28 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
|
235
250
|
end
|
|
236
251
|
end",
|
|
237
252
|
|
|
253
|
+
add_foreign_key_mysql:
|
|
254
|
+
"Adding a foreign key blocks writes on both tables. If you are 100% sure
|
|
255
|
+
all rows are valid and migrations do not use a connection pooler,
|
|
256
|
+
you can add the foreign key without validating existing rows.
|
|
257
|
+
|
|
258
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
259
|
+
def up
|
|
260
|
+
safety_assured do
|
|
261
|
+
begin
|
|
262
|
+
execute \"SET SESSION foreign_key_checks = 0\"
|
|
263
|
+
%{add_foreign_key_code}
|
|
264
|
+
ensure
|
|
265
|
+
execute \"SET SESSION foreign_key_checks = 1\"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def down
|
|
271
|
+
%{remove_foreign_key_code}
|
|
272
|
+
end
|
|
273
|
+
end",
|
|
274
|
+
|
|
238
275
|
validate_foreign_key:
|
|
239
276
|
"Validating a foreign key while writes are blocked is dangerous.
|
|
240
277
|
Use disable_ddl_transaction! or a separate migration.",
|