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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c7db529b15ac630f2f6c9e284b185ca5affbdb275c2d34d26e508317f6e2fa4
4
- data.tar.gz: 8b00a79a0f4cb787d43de827cbafe495c081b5250db4f08d75a893953778d72a
3
+ metadata.gz: 250e255ba679a0e581fb86ed13b2824cf5749b7dc3ac5bf0a26bfffebe314fb8
4
+ data.tar.gz: d91882cb145d2f59a688e41e0d20301c6ca41d3e5e9790be9996eb256cc26dd2
5
5
  SHA512:
6
- metadata.gz: 73091c6f13637427c09ed2dc013f804ac1ce91d78844b6dc10b7deb89aeb4d391123c4f16e661b34a1fd9e8013f5c6c50295e2a45824ca042e9418d3c6cd338d
7
- data.tar.gz: 96bbd494942e1a9f3a529f2ee7453fb693e9ec61aba22846d6be9c5a986b9213053142f9f06a87dc16dbc9361520ae9ae84e32c72e43654fdd0f2da921590cdb
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Bob Remeika and David Waller, 2015-2025 Andrew Kane
1
+ Copyright (c) 2013 Bob Remeika and David Waller, 2015-2026 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
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 in Postgres 12+
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 in Postgres 12+
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
- ### Adding a column with a volatile default value
689
+ ### Renaming a schema
626
690
 
627
691
  #### Bad
628
692
 
629
- 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.
693
+ Renaming a schema that’s in use will cause errors in your application.
630
694
 
631
695
  ```ruby
632
- class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
696
+ class RenameUsersToCustomers < ActiveRecord::Migration[8.1]
633
697
  def change
634
- add_column :users, :some_column, :uuid, default: "gen_random_uuid()"
698
+ rename_schema :users, :customers
635
699
  end
636
700
  end
637
701
  ```
638
702
 
639
703
  #### Good
640
704
 
641
- Instead, add the column without a default value, then change the default.
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 AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
645
- def up
646
- add_column :users, :some_column, :uuid
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
- def down
651
- remove_column :users, :some_column
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
- Then [backfill the data](#backfilling-data).
742
+ ### Using shared or exclusive locking
657
743
 
658
- ### Renaming a schema
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
- Renaming a schema that’s in use will cause errors in your application.
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 RenameUsersToCustomers < ActiveRecord::Migration[8.1]
775
+ class AddSomeColumnToUsers < ActiveRecord::Migration[8.1]
666
776
  def change
667
- rename_schema :users, :customers
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
- A safer approach is to:
784
+ Instead, add the column without a default value, then change the default.
675
785
 
676
- 1. Create a new schema
677
- 2. Write to both schemas
678
- 3. Backfill data from the old schema to the new schema
679
- 4. Move reads from the old schema to the new schema
680
- 5. Stop writing to the old schema
681
- 6. Drop the old schema
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 = 10 # or 8.0, 10.5, etc
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: 13, catalog: 15}
1043
+ StrongMigrations.target_version = {primary: 16, catalog: 18}
924
1044
  ```
925
1045
 
926
1046
  ## Analyze Tables
@@ -24,9 +24,9 @@ module StrongMigrations
24
24
  when /mysql|trilogy/
25
25
  # could try to connect to database and check for MariaDB
26
26
  # but this should be fine
27
- "8.0"
27
+ "8.4"
28
28
  else
29
- "10"
29
+ "18"
30
30
  end
31
31
  end
32
32
 
@@ -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
- # Check key since DEFAULT NULL behaves differently from no default
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
- # Also, Active Record has special case for uuid columns that allows function default values
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: (volatile ? "volatile" : "non-null")
56
- elsif default.is_a?(Proc) && postgresql?
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[:index] = options[:index].merge(algorithm: :concurrently)
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(:add_index, [table, column, {unique: true, algorithm: :concurrently}]),
194
- constraint_command: command_str(:add_unique_constraint, [table, {using_index: index_name}]),
195
- remove_command: command_str(:remove_unique_constraint, [table, column])
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(:remove_check_constraint, [table, c.expression, {name: c.name}])
293
+ change_commands << command_str("remove_check_constraint", [table, c.expression, {name: c.name}])
228
294
  end
229
- change_commands << command_str(:change_column, args + [options])
295
+ change_commands << command_str("change_column", args + [options])
230
296
  constraints.each do |c|
231
- change_commands << command_str(:add_check_constraint, [table, c.expression, {name: c.name, validate: false}])
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(:validate_check_constraint, [table, {name: c.name}])
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(:add_check_constraint, add_args)
360
+ add_constraint_code = command_str("add_check_constraint", add_args)
290
361
 
291
- up_code = String.new(command_str(:validate_check_constraint, validate_args))
292
- up_code << "\n #{command_str(:change_column_null, change_args)}"
293
- up_code << "\n #{command_str(:remove_check_constraint, remove_args)}"
294
- down_code = "#{add_constraint_code}\n #{command_str(:change_column_null, [table, column, true])}"
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
- Please make really sure you're not calling a VOLATILE function,
31
- then wrap it in a safety_assured { ... } block.",
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
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "2.5.2"
2
+ VERSION = "2.7.0"
3
3
  end
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.5.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.1'
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.1'
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.2'
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: 3.6.9
75
+ rubygems_version: 4.0.6
76
76
  specification_version: 4
77
77
  summary: Catch unsafe migrations in development
78
78
  test_files: []