strong_migrations 2.5.1 → 2.6.0

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