strong_migrations 2.5.2 → 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 +11 -0
- data/LICENSE.txt +1 -1
- data/README.md +200 -80
- data/lib/generators/strong_migrations/install_generator.rb +2 -2
- data/lib/strong_migrations/checks.rb +114 -21
- data/lib/strong_migrations/error_messages.rb +57 -2
- 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: 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
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 2.7.0 (2026-04-25)
|
|
2
|
+
|
|
3
|
+
- Added check for `add_foreign_key` with MySQL and MariaDB
|
|
4
|
+
- Added check for `add_column` with callable default value with MySQL and MariaDB
|
|
5
|
+
|
|
6
|
+
## 2.6.0 (2026-04-07)
|
|
7
|
+
|
|
8
|
+
- Added check for `algorithm: :copy` with MySQL and MariaDB
|
|
9
|
+
- Added check for `lock: :shared` and `lock: :exclusive` with MySQL and MariaDB
|
|
10
|
+
- Dropped support for Ruby < 3.3 and Active Record < 7.2
|
|
11
|
+
|
|
1
12
|
## 2.5.2 (2025-12-20)
|
|
2
13
|
|
|
3
14
|
- Fixed false positive for `add_reference` with `foreign_key: {validate: false}`
|
data/LICENSE.txt
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,14 +75,19 @@ 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
|
+
MySQL and MariaDB-specific checks:
|
|
86
|
+
|
|
87
|
+
- [using the COPY algorithm](#using-the-copy-algorithm)
|
|
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)
|
|
90
|
+
|
|
85
91
|
Best practices:
|
|
86
92
|
|
|
87
93
|
- [keeping non-unique indexes to three columns or less](#keeping-non-unique-indexes-to-three-columns-or-less)
|
|
@@ -146,14 +152,14 @@ Type | Safe Changes
|
|
|
146
152
|
--- | ---
|
|
147
153
|
`cidr` | Changing to `inet`
|
|
148
154
|
`citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
|
|
149
|
-
`datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC
|
|
155
|
+
`datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC
|
|
150
156
|
`decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
|
151
157
|
`interval` | Increasing or removing `:precision`
|
|
152
158
|
`numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
|
153
159
|
`string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
|
|
154
160
|
`text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
|
|
155
161
|
`time` | Increasing or removing `:precision`
|
|
156
|
-
`timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC
|
|
162
|
+
`timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC
|
|
157
163
|
|
|
158
164
|
And some in MySQL and MariaDB:
|
|
159
165
|
|
|
@@ -292,9 +298,80 @@ end
|
|
|
292
298
|
|
|
293
299
|
Add a non-generated column and use callbacks or triggers instead (or a virtual generated column with MySQL and MariaDB).
|
|
294
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
|
+
|
|
295
372
|
### Adding a check constraint
|
|
296
373
|
|
|
297
|
-
:turtle: Safe by default available
|
|
374
|
+
:turtle: Safe by default available for Postgres
|
|
298
375
|
|
|
299
376
|
#### Bad
|
|
300
377
|
|
|
@@ -384,6 +461,8 @@ end
|
|
|
384
461
|
|
|
385
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.
|
|
386
463
|
|
|
464
|
+
## Postgres Checks
|
|
465
|
+
|
|
387
466
|
### Adding an index non-concurrently
|
|
388
467
|
|
|
389
468
|
:turtle: Safe by default available
|
|
@@ -452,54 +531,6 @@ class AddReferenceToUsers < ActiveRecord::Migration[8.1]
|
|
|
452
531
|
end
|
|
453
532
|
```
|
|
454
533
|
|
|
455
|
-
### Adding a foreign key
|
|
456
|
-
|
|
457
|
-
:turtle: Safe by default available
|
|
458
|
-
|
|
459
|
-
#### Bad
|
|
460
|
-
|
|
461
|
-
In Postgres, adding a foreign key blocks writes on both tables.
|
|
462
|
-
|
|
463
|
-
```ruby
|
|
464
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
465
|
-
def change
|
|
466
|
-
add_foreign_key :users, :orders
|
|
467
|
-
end
|
|
468
|
-
end
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
or
|
|
472
|
-
|
|
473
|
-
```ruby
|
|
474
|
-
class AddReferenceToUsers < ActiveRecord::Migration[8.1]
|
|
475
|
-
def change
|
|
476
|
-
add_reference :users, :order, foreign_key: true
|
|
477
|
-
end
|
|
478
|
-
end
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
#### Good
|
|
482
|
-
|
|
483
|
-
Add the foreign key without validating existing rows:
|
|
484
|
-
|
|
485
|
-
```ruby
|
|
486
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
487
|
-
def change
|
|
488
|
-
add_foreign_key :users, :orders, validate: false
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
Then validate them in a separate migration.
|
|
494
|
-
|
|
495
|
-
```ruby
|
|
496
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.1]
|
|
497
|
-
def change
|
|
498
|
-
validate_foreign_key :users, :orders
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
```
|
|
502
|
-
|
|
503
534
|
### Adding a unique constraint
|
|
504
535
|
|
|
505
536
|
#### Bad
|
|
@@ -577,6 +608,39 @@ class AddPropertiesToUsers < ActiveRecord::Migration[8.1]
|
|
|
577
608
|
end
|
|
578
609
|
```
|
|
579
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
|
+
|
|
580
644
|
### Setting NOT NULL on an existing column
|
|
581
645
|
|
|
582
646
|
:turtle: Safe by default available
|
|
@@ -622,63 +686,119 @@ class ValidateSomeColumnNotNull < ActiveRecord::Migration[8.1]
|
|
|
622
686
|
end
|
|
623
687
|
```
|
|
624
688
|
|
|
625
|
-
###
|
|
689
|
+
### Renaming a schema
|
|
626
690
|
|
|
627
691
|
#### Bad
|
|
628
692
|
|
|
629
|
-
|
|
693
|
+
Renaming a schema that’s in use will cause errors in your application.
|
|
630
694
|
|
|
631
695
|
```ruby
|
|
632
|
-
class
|
|
696
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[8.1]
|
|
633
697
|
def change
|
|
634
|
-
|
|
698
|
+
rename_schema :users, :customers
|
|
635
699
|
end
|
|
636
700
|
end
|
|
637
701
|
```
|
|
638
702
|
|
|
639
703
|
#### Good
|
|
640
704
|
|
|
641
|
-
|
|
705
|
+
A safer approach is to:
|
|
706
|
+
|
|
707
|
+
1. Create a new schema
|
|
708
|
+
2. Write to both schemas
|
|
709
|
+
3. Backfill data from the old schema to the new schema
|
|
710
|
+
4. Move reads from the old schema to the new schema
|
|
711
|
+
5. Stop writing to the old schema
|
|
712
|
+
6. Drop the old schema
|
|
713
|
+
|
|
714
|
+
## MySQL and MariaDB Checks
|
|
715
|
+
|
|
716
|
+
### Using the COPY algorithm
|
|
717
|
+
|
|
718
|
+
#### Bad
|
|
719
|
+
|
|
720
|
+
In MySQL and MariaDB, using the `COPY` algorithm blocks writes.
|
|
642
721
|
|
|
643
722
|
```ruby
|
|
644
|
-
class
|
|
645
|
-
def
|
|
646
|
-
|
|
647
|
-
change_column_default :users, :some_column, from: nil, to: "gen_random_uuid()"
|
|
723
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
|
|
724
|
+
def change
|
|
725
|
+
add_index :users, :some_column, algorithm: :copy
|
|
648
726
|
end
|
|
727
|
+
end
|
|
728
|
+
```
|
|
649
729
|
|
|
650
|
-
|
|
651
|
-
|
|
730
|
+
#### Good
|
|
731
|
+
|
|
732
|
+
Use the default algorithm.
|
|
733
|
+
|
|
734
|
+
```ruby
|
|
735
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
|
|
736
|
+
def change
|
|
737
|
+
add_index :users, :some_column
|
|
652
738
|
end
|
|
653
739
|
end
|
|
654
740
|
```
|
|
655
741
|
|
|
656
|
-
|
|
742
|
+
### Using shared or exclusive locking
|
|
657
743
|
|
|
658
|
-
|
|
744
|
+
#### Bad
|
|
745
|
+
|
|
746
|
+
In MySQL and MariaDB, using shared locking blocks writes, and using exclusive locking blocks reads and writes.
|
|
747
|
+
|
|
748
|
+
```ruby
|
|
749
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.2]
|
|
750
|
+
def change
|
|
751
|
+
add_index :users, :some_column, lock: :shared
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
#### Good
|
|
757
|
+
|
|
758
|
+
Use the default locking or no locking.
|
|
759
|
+
|
|
760
|
+
```ruby
|
|
761
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.2]
|
|
762
|
+
def change
|
|
763
|
+
add_index :users, :some_column
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Adding a column with an expression default value
|
|
659
769
|
|
|
660
770
|
#### Bad
|
|
661
771
|
|
|
662
|
-
|
|
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.
|
|
663
773
|
|
|
664
774
|
```ruby
|
|
665
|
-
class
|
|
775
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
|
|
666
776
|
def change
|
|
667
|
-
|
|
777
|
+
add_column :users, :some_column, :datetime, default: -> { "(now())" }
|
|
668
778
|
end
|
|
669
779
|
end
|
|
670
780
|
```
|
|
671
781
|
|
|
672
782
|
#### Good
|
|
673
783
|
|
|
674
|
-
|
|
784
|
+
Instead, add the column without a default value, then change the default.
|
|
675
785
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
|
682
802
|
|
|
683
803
|
### Keeping non-unique indexes to three columns or less
|
|
684
804
|
|
|
@@ -819,7 +939,7 @@ ALTER ROLE myuser SET lock_timeout = '10s';
|
|
|
819
939
|
ALTER ROLE myuser SET statement_timeout = '1h';
|
|
820
940
|
```
|
|
821
941
|
|
|
822
|
-
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.
|
|
823
943
|
|
|
824
944
|
## App Timeouts
|
|
825
945
|
|
|
@@ -835,7 +955,7 @@ production:
|
|
|
835
955
|
lock_timeout: 10s
|
|
836
956
|
```
|
|
837
957
|
|
|
838
|
-
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.
|
|
839
959
|
|
|
840
960
|
For MySQL:
|
|
841
961
|
|
|
@@ -910,7 +1030,7 @@ Use the version from your latest migration.
|
|
|
910
1030
|
If your development database version is different from production, you can specify the production version so the right checks run in development.
|
|
911
1031
|
|
|
912
1032
|
```ruby
|
|
913
|
-
StrongMigrations.target_version =
|
|
1033
|
+
StrongMigrations.target_version = 16
|
|
914
1034
|
```
|
|
915
1035
|
|
|
916
1036
|
The major version works well for Postgres, while the major and minor version is recommended for MySQL and MariaDB.
|
|
@@ -920,7 +1040,7 @@ For safety, this option only affects development and test environments. In other
|
|
|
920
1040
|
If your app has multiple databases with different versions, you can use:
|
|
921
1041
|
|
|
922
1042
|
```ruby
|
|
923
|
-
StrongMigrations.target_version = {primary:
|
|
1043
|
+
StrongMigrations.target_version = {primary: 16, catalog: 18}
|
|
924
1044
|
```
|
|
925
1045
|
|
|
926
1046
|
## Analyze Tables
|
|
@@ -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?
|
|
@@ -76,6 +86,11 @@ module StrongMigrations
|
|
|
76
86
|
rewrite_blocks: adapter.rewrite_blocks,
|
|
77
87
|
append: append
|
|
78
88
|
end
|
|
89
|
+
|
|
90
|
+
check_algorithm_option("add_column", *args, **options)
|
|
91
|
+
|
|
92
|
+
# not necessarily dangerous, but not necessary
|
|
93
|
+
check_lock_option("add_column", *args, **options)
|
|
79
94
|
end
|
|
80
95
|
|
|
81
96
|
def check_add_exclusion_constraint(*args)
|
|
@@ -112,6 +127,11 @@ module StrongMigrations
|
|
|
112
127
|
raise_error :add_foreign_key,
|
|
113
128
|
add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
|
|
114
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])
|
|
115
135
|
end
|
|
116
136
|
end
|
|
117
137
|
|
|
@@ -138,6 +158,10 @@ module StrongMigrations
|
|
|
138
158
|
|
|
139
159
|
raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
|
|
140
160
|
end
|
|
161
|
+
|
|
162
|
+
check_algorithm_option("add_index", *args, **options)
|
|
163
|
+
|
|
164
|
+
check_lock_option("add_index", *args, **options)
|
|
141
165
|
end
|
|
142
166
|
|
|
143
167
|
def check_add_reference(method, *args)
|
|
@@ -155,7 +179,7 @@ module StrongMigrations
|
|
|
155
179
|
|
|
156
180
|
if index_unsafe || foreign_key_unsafe
|
|
157
181
|
if index_value.is_a?(Hash)
|
|
158
|
-
options
|
|
182
|
+
options = options.merge(index: index_value.merge(algorithm: :concurrently))
|
|
159
183
|
elsif index_value
|
|
160
184
|
options = options.merge(index: {algorithm: :concurrently})
|
|
161
185
|
end
|
|
@@ -178,6 +202,48 @@ module StrongMigrations
|
|
|
178
202
|
command: command_str(method, [table, reference, options]),
|
|
179
203
|
append: append
|
|
180
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
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
check_algorithm_option("add_reference", *args, **options)
|
|
215
|
+
|
|
216
|
+
# not necessarily dangerous, but not necessary
|
|
217
|
+
check_lock_option("add_reference", *args, **options)
|
|
218
|
+
|
|
219
|
+
if (mysql? || mariadb?) && !new_table?(table)
|
|
220
|
+
index_value = options[:index]
|
|
221
|
+
copy_set = index_value.is_a?(Hash) && index_value[:algorithm] == :copy
|
|
222
|
+
if copy_set
|
|
223
|
+
index_value = index_value.except(:algorithm)
|
|
224
|
+
if index_value.empty?
|
|
225
|
+
options = options.except(:index)
|
|
226
|
+
else
|
|
227
|
+
options = options.merge(index: index_value)
|
|
228
|
+
end
|
|
229
|
+
raise_error :copy_algorithm, command: command_str("add_reference", args + [options])
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if ar_version >= 8.2
|
|
233
|
+
lock = index_value.is_a?(Hash) && index_value[:lock]
|
|
234
|
+
if [:shared, :exclusive].include?(lock)
|
|
235
|
+
index_value = index_value.except(:lock)
|
|
236
|
+
if index_value.empty?
|
|
237
|
+
options = options.except(:index)
|
|
238
|
+
else
|
|
239
|
+
options = options.merge(index: index_value)
|
|
240
|
+
end
|
|
241
|
+
raise_error :lock_option,
|
|
242
|
+
command: command_str(method, args + [options]),
|
|
243
|
+
lock_type: lock.to_s,
|
|
244
|
+
lock_blocks: lock == :shared ? "reads" : "reads and writes"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
181
247
|
end
|
|
182
248
|
end
|
|
183
249
|
|
|
@@ -190,9 +256,9 @@ module StrongMigrations
|
|
|
190
256
|
if column && !new_table?(table)
|
|
191
257
|
index_name = connection.index_name(table, {column: column})
|
|
192
258
|
raise_error :add_unique_constraint,
|
|
193
|
-
index_command: command_str(
|
|
194
|
-
constraint_command: command_str(
|
|
195
|
-
remove_command: command_str(
|
|
259
|
+
index_command: command_str("add_index", [table, column, {unique: true, algorithm: :concurrently}]),
|
|
260
|
+
constraint_command: command_str("add_unique_constraint", [table, {using_index: index_name}]),
|
|
261
|
+
remove_command: command_str("remove_unique_constraint", [table, column])
|
|
196
262
|
end
|
|
197
263
|
end
|
|
198
264
|
|
|
@@ -224,16 +290,16 @@ module StrongMigrations
|
|
|
224
290
|
if constraints.any?
|
|
225
291
|
change_commands = []
|
|
226
292
|
constraints.each do |c|
|
|
227
|
-
change_commands << command_str(
|
|
293
|
+
change_commands << command_str("remove_check_constraint", [table, c.expression, {name: c.name}])
|
|
228
294
|
end
|
|
229
|
-
change_commands << command_str(
|
|
295
|
+
change_commands << command_str("change_column", args + [options])
|
|
230
296
|
constraints.each do |c|
|
|
231
|
-
change_commands << command_str(
|
|
297
|
+
change_commands << command_str("add_check_constraint", [table, c.expression, {name: c.name, validate: false}])
|
|
232
298
|
end
|
|
233
299
|
|
|
234
300
|
validate_commands = []
|
|
235
301
|
constraints.each do |c|
|
|
236
|
-
validate_commands << command_str(
|
|
302
|
+
validate_commands << command_str("validate_check_constraint", [table, {name: c.name}])
|
|
237
303
|
end
|
|
238
304
|
|
|
239
305
|
raise_error :change_column_constraint,
|
|
@@ -241,6 +307,11 @@ module StrongMigrations
|
|
|
241
307
|
validate_constraint_code: validate_commands.join("\n ")
|
|
242
308
|
end
|
|
243
309
|
end
|
|
310
|
+
|
|
311
|
+
check_algorithm_option("change_column", *args, **options)
|
|
312
|
+
|
|
313
|
+
# not necessarily dangerous, but not necessary
|
|
314
|
+
check_lock_option("change_column", *args, **options)
|
|
244
315
|
end
|
|
245
316
|
|
|
246
317
|
def check_change_column_default(*args)
|
|
@@ -286,12 +357,12 @@ module StrongMigrations
|
|
|
286
357
|
throw :safe
|
|
287
358
|
end
|
|
288
359
|
|
|
289
|
-
add_constraint_code = command_str(
|
|
360
|
+
add_constraint_code = command_str("add_check_constraint", add_args)
|
|
290
361
|
|
|
291
|
-
up_code = String.new(command_str(
|
|
292
|
-
up_code << "\n #{command_str(
|
|
293
|
-
up_code << "\n #{command_str(
|
|
294
|
-
down_code = "#{add_constraint_code}\n #{command_str(
|
|
362
|
+
up_code = String.new(command_str("validate_check_constraint", validate_args))
|
|
363
|
+
up_code << "\n #{command_str("change_column_null", change_args)}"
|
|
364
|
+
up_code << "\n #{command_str("remove_check_constraint", remove_args)}"
|
|
365
|
+
down_code = "#{add_constraint_code}\n #{command_str("change_column_null", [table, column, true])}"
|
|
295
366
|
validate_constraint_code = "def up\n #{up_code}\n end\n\n def down\n #{down_code}\n end"
|
|
296
367
|
|
|
297
368
|
raise_error :change_column_null_postgresql,
|
|
@@ -337,6 +408,7 @@ module StrongMigrations
|
|
|
337
408
|
raise_error :execute, header: "Possibly dangerous operation"
|
|
338
409
|
end
|
|
339
410
|
|
|
411
|
+
# supports algorithm and lock options, but always raises
|
|
340
412
|
def check_remove_column(method, *args)
|
|
341
413
|
columns =
|
|
342
414
|
case method
|
|
@@ -383,8 +455,14 @@ module StrongMigrations
|
|
|
383
455
|
|
|
384
456
|
raise_error :remove_index, command: command_str("remove_index", args + [options.merge(algorithm: :concurrently)])
|
|
385
457
|
end
|
|
458
|
+
|
|
459
|
+
check_algorithm_option("remove_index", *args, **options)
|
|
460
|
+
|
|
461
|
+
# not necessarily dangerous, but not necessary
|
|
462
|
+
check_lock_option("remove_index", *args, **options)
|
|
386
463
|
end
|
|
387
464
|
|
|
465
|
+
# supports algorithm and lock options, but always raises
|
|
388
466
|
def check_rename_column
|
|
389
467
|
raise_error :rename_column
|
|
390
468
|
end
|
|
@@ -442,6 +520,21 @@ module StrongMigrations
|
|
|
442
520
|
@migration.stop!(message, header: header || "Dangerous operation detected")
|
|
443
521
|
end
|
|
444
522
|
|
|
523
|
+
def check_algorithm_option(method, *args, **options)
|
|
524
|
+
if (mysql? || mariadb?) && options[:algorithm] == :copy && !new_table?(args[0]) && (ar_version >= 8.2 || method == "add_index")
|
|
525
|
+
raise_error :copy_algorithm, command: command_str(method, args + [options.except(:algorithm)])
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def check_lock_option(method, *args, **options)
|
|
530
|
+
if (mysql? || mariadb?) && [:shared, :exclusive].include?(options[:lock]) && !new_table?(args[0]) && ar_version >= 8.2
|
|
531
|
+
raise_error :lock_option,
|
|
532
|
+
command: command_str(method, args + [options.except(:lock)]),
|
|
533
|
+
lock_type: options[:lock].to_s,
|
|
534
|
+
lock_blocks: options[:lock] == :shared ? "reads" : "reads and writes"
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
445
538
|
def constraint_str(statement, identifiers)
|
|
446
539
|
# not all identifiers are tables, but this method of quoting should be fine
|
|
447
540
|
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
|
@@ -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.",
|
|
@@ -281,6 +318,24 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
|
281
318
|
def down
|
|
282
319
|
%{remove_command}
|
|
283
320
|
end
|
|
321
|
+
end",
|
|
322
|
+
|
|
323
|
+
copy_algorithm:
|
|
324
|
+
"Using the COPY algorithm blocks writes. Instead, use:
|
|
325
|
+
|
|
326
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
327
|
+
def change
|
|
328
|
+
%{command}
|
|
329
|
+
end
|
|
330
|
+
end",
|
|
331
|
+
|
|
332
|
+
lock_option:
|
|
333
|
+
"Using %{lock_type} locking blocks %{lock_blocks}. Instead, use:
|
|
334
|
+
|
|
335
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
336
|
+
def change
|
|
337
|
+
%{command}
|
|
338
|
+
end
|
|
284
339
|
end"
|
|
285
340
|
}
|
|
286
341
|
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.7.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: []
|