strong_migrations 2.0.2 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98ed2c92ee055824eefbb8239b65a6ef1c847b1ffa87e8cafb7fce34f8878c3c
4
- data.tar.gz: 4f20acd9b50d07dfbe8ff6745f2fda6aaf7ca6731be72c8275929e15b12bd212
3
+ metadata.gz: e155e036c26788c7c9fa7f513131422b784b895fe1cbfdfc8052ad1f5233cd6c
4
+ data.tar.gz: 5fe21cada2e29c433313c686332d96c67e2c55d057fd6c68839fe1993307c0f8
5
5
  SHA512:
6
- metadata.gz: 492d1dec59fc74196c3670b8a49fcc080af36eb3297c1c9aabfea7f0ff1943c21a4fe43e66d0e406d4eae1b785223f3e137bf05ee377d5597e87573f7b7c7af4
7
- data.tar.gz: 2a955f9eb53386c39678f48291603345be5d086877ce9a2fdd2f60dcc5eec84b671945fd9dba6a4f5d98d9d41da7cb32ab061daa333f461344ae7b9c7884c9d0
6
+ metadata.gz: 7f40abaf84f10ce6ce7255d083b3ec0940c2f7380705857767b0cde56b23802dcf844c27251c634006559dfb4489b799ab3758bf9928b6cb57b6bceb1bf051ec
7
+ data.tar.gz: c283b8b93715a4aabd2c0e8dc15b022e45ebfbe3bb52fccc037b9c95d3b2279d6002b78354cab8600cf2972f96a6966af0fd80aa9b58cde53343b5035d8bc59c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 2.2.0 (2025-02-01)
2
+
3
+ - Fixed constraint name for long table and column names with `change_column_null`
4
+ - Dropped support for Active Record < 7
5
+
6
+ ## 2.1.0 (2024-11-08)
7
+
8
+ - Added `skip_database` method
9
+ - Added experimental `remove_invalid_indexes` option
10
+ - Added warning for unsupported adapters
11
+ - Improved output for `db:forward`, `db:rollback`, `db:migrate:up`, and `db:migrate:down`
12
+ - Made operations more retriable with `safe_by_default`
13
+
1
14
  ## 2.0.2 (2024-10-30)
2
15
 
3
16
  - Fixed migrations not running with Active Record 8 rc2
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Bob Remeika and David Waller, 2015-2024 Andrew Kane
1
+ Copyright (c) 2013 Bob Remeika and David Waller, 2015-2025 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[7.2]
46
+ class RemoveColumn < ActiveRecord::Migration[8.0]
47
47
  def change
48
48
  safety_assured { remove_column :users, :name }
49
49
  end
@@ -98,7 +98,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
98
98
  Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
99
99
 
100
100
  ```ruby
101
- class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.2]
101
+ class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.0]
102
102
  def change
103
103
  remove_column :users, :some_column
104
104
  end
@@ -119,7 +119,7 @@ end
119
119
  3. Write a migration to remove the column (wrap in `safety_assured` block)
120
120
 
121
121
  ```ruby
122
- class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.2]
122
+ class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.0]
123
123
  def change
124
124
  safety_assured { remove_column :users, :some_column }
125
125
  end
@@ -136,7 +136,7 @@ end
136
136
  Changing the type of a column causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
137
137
 
138
138
  ```ruby
139
- class ChangeSomeColumnType < ActiveRecord::Migration[7.2]
139
+ class ChangeSomeColumnType < ActiveRecord::Migration[8.0]
140
140
  def change
141
141
  change_column :users, :some_column, :new_type
142
142
  end
@@ -182,7 +182,7 @@ A safer approach is to:
182
182
  Renaming a column that’s in use will cause errors in your application.
183
183
 
184
184
  ```ruby
185
- class RenameSomeColumn < ActiveRecord::Migration[7.2]
185
+ class RenameSomeColumn < ActiveRecord::Migration[8.0]
186
186
  def change
187
187
  rename_column :users, :some_column, :new_name
188
188
  end
@@ -207,7 +207,7 @@ A safer approach is to:
207
207
  Renaming a table that’s in use will cause errors in your application.
208
208
 
209
209
  ```ruby
210
- class RenameUsersToCustomers < ActiveRecord::Migration[7.2]
210
+ class RenameUsersToCustomers < ActiveRecord::Migration[8.0]
211
211
  def change
212
212
  rename_table :users, :customers
213
213
  end
@@ -232,7 +232,7 @@ A safer approach is to:
232
232
  The `force` option can drop an existing table.
233
233
 
234
234
  ```ruby
235
- class CreateUsers < ActiveRecord::Migration[7.2]
235
+ class CreateUsers < ActiveRecord::Migration[8.0]
236
236
  def change
237
237
  create_table :users, force: true do |t|
238
238
  # ...
@@ -246,7 +246,7 @@ end
246
246
  Create tables without the `force` option.
247
247
 
248
248
  ```ruby
249
- class CreateUsers < ActiveRecord::Migration[7.2]
249
+ class CreateUsers < ActiveRecord::Migration[8.0]
250
250
  def change
251
251
  create_table :users do |t|
252
252
  # ...
@@ -264,7 +264,7 @@ If you intend to drop an existing table, run `drop_table` first.
264
264
  Adding an auto-incrementing column (`serial`/`bigserial` in Postgres and `AUTO_INCREMENT` in MySQL and MariaDB) causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
265
265
 
266
266
  ```ruby
267
- class AddIdToCitiesUsers < ActiveRecord::Migration[7.2]
267
+ class AddIdToCitiesUsers < ActiveRecord::Migration[8.0]
268
268
  def change
269
269
  add_column :cities_users, :id, :primary_key
270
270
  end
@@ -284,7 +284,7 @@ Create a new table and migrate the data with the same steps as [renaming a table
284
284
  Adding a stored generated column causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
285
285
 
286
286
  ```ruby
287
- class AddSomeColumnToUsers < ActiveRecord::Migration[7.2]
287
+ class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
288
288
  def change
289
289
  add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
290
290
  end
@@ -304,7 +304,7 @@ Add a non-generated column and use callbacks or triggers instead (or a virtual g
304
304
  Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
305
305
 
306
306
  ```ruby
307
- class AddCheckConstraint < ActiveRecord::Migration[7.2]
307
+ class AddCheckConstraint < ActiveRecord::Migration[8.0]
308
308
  def change
309
309
  add_check_constraint :users, "price > 0", name: "price_check"
310
310
  end
@@ -316,7 +316,7 @@ end
316
316
  Add the check constraint without validating existing rows:
317
317
 
318
318
  ```ruby
319
- class AddCheckConstraint < ActiveRecord::Migration[7.2]
319
+ class AddCheckConstraint < ActiveRecord::Migration[8.0]
320
320
  def change
321
321
  add_check_constraint :users, "price > 0", name: "price_check", validate: false
322
322
  end
@@ -326,7 +326,7 @@ end
326
326
  Then validate them in a separate migration.
327
327
 
328
328
  ```ruby
329
- class ValidateCheckConstraint < ActiveRecord::Migration[7.2]
329
+ class ValidateCheckConstraint < ActiveRecord::Migration[8.0]
330
330
  def change
331
331
  validate_check_constraint :users, name: "price_check"
332
332
  end
@@ -342,7 +342,7 @@ end
342
342
  Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
343
343
 
344
344
  ```ruby
345
- class ExecuteSQL < ActiveRecord::Migration[7.2]
345
+ class ExecuteSQL < ActiveRecord::Migration[8.0]
346
346
  def change
347
347
  safety_assured { execute "..." }
348
348
  end
@@ -351,12 +351,14 @@ end
351
351
 
352
352
  ### Backfilling data
353
353
 
354
+ Note: Strong Migrations does not detect dangerous backfills.
355
+
354
356
  #### Bad
355
357
 
356
358
  Active Record creates a transaction around each migration, and backfilling in the same transaction that alters a table keeps the table locked for the [duration of the backfill](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/).
357
359
 
358
360
  ```ruby
359
- class AddSomeColumnToUsers < ActiveRecord::Migration[7.2]
361
+ class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
360
362
  def change
361
363
  add_column :users, :some_column, :text
362
364
  User.update_all some_column: "default_value"
@@ -371,7 +373,7 @@ Also, running a single query to update data can cause issues for large tables.
371
373
  There are three keys to backfilling safely: batching, throttling, and running it outside a transaction. Use the Rails console or a separate migration with `disable_ddl_transaction!`.
372
374
 
373
375
  ```ruby
374
- class BackfillSomeColumn < ActiveRecord::Migration[7.2]
376
+ class BackfillSomeColumn < ActiveRecord::Migration[8.0]
375
377
  disable_ddl_transaction!
376
378
 
377
379
  def up
@@ -383,6 +385,8 @@ class BackfillSomeColumn < ActiveRecord::Migration[7.2]
383
385
  end
384
386
  ```
385
387
 
388
+ Note: If backfilling with a method other than `update_all`, use `User.reset_column_information` to ensure the model has up-to-date column information.
389
+
386
390
  ### Adding an index non-concurrently
387
391
 
388
392
  :turtle: Safe by default available
@@ -392,7 +396,7 @@ end
392
396
  In Postgres, adding an index non-concurrently blocks writes.
393
397
 
394
398
  ```ruby
395
- class AddSomeIndexToUsers < ActiveRecord::Migration[7.2]
399
+ class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
396
400
  def change
397
401
  add_index :users, :some_column
398
402
  end
@@ -404,7 +408,7 @@ end
404
408
  Add indexes concurrently.
405
409
 
406
410
  ```ruby
407
- class AddSomeIndexToUsers < ActiveRecord::Migration[7.2]
411
+ class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
408
412
  disable_ddl_transaction!
409
413
 
410
414
  def change
@@ -430,7 +434,7 @@ rails g index table column
430
434
  Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
431
435
 
432
436
  ```ruby
433
- class AddReferenceToUsers < ActiveRecord::Migration[7.2]
437
+ class AddReferenceToUsers < ActiveRecord::Migration[8.0]
434
438
  def change
435
439
  add_reference :users, :city
436
440
  end
@@ -442,7 +446,7 @@ end
442
446
  Make sure the index is added concurrently.
443
447
 
444
448
  ```ruby
445
- class AddReferenceToUsers < ActiveRecord::Migration[7.2]
449
+ class AddReferenceToUsers < ActiveRecord::Migration[8.0]
446
450
  disable_ddl_transaction!
447
451
 
448
452
  def change
@@ -460,7 +464,7 @@ end
460
464
  In Postgres, adding a foreign key blocks writes on both tables.
461
465
 
462
466
  ```ruby
463
- class AddForeignKeyOnUsers < ActiveRecord::Migration[7.2]
467
+ class AddForeignKeyOnUsers < ActiveRecord::Migration[8.0]
464
468
  def change
465
469
  add_foreign_key :users, :orders
466
470
  end
@@ -470,7 +474,7 @@ end
470
474
  or
471
475
 
472
476
  ```ruby
473
- class AddReferenceToUsers < ActiveRecord::Migration[7.2]
477
+ class AddReferenceToUsers < ActiveRecord::Migration[8.0]
474
478
  def change
475
479
  add_reference :users, :order, foreign_key: true
476
480
  end
@@ -482,7 +486,7 @@ end
482
486
  Add the foreign key without validating existing rows:
483
487
 
484
488
  ```ruby
485
- class AddForeignKeyOnUsers < ActiveRecord::Migration[7.2]
489
+ class AddForeignKeyOnUsers < ActiveRecord::Migration[8.0]
486
490
  def change
487
491
  add_foreign_key :users, :orders, validate: false
488
492
  end
@@ -492,7 +496,7 @@ end
492
496
  Then validate them in a separate migration.
493
497
 
494
498
  ```ruby
495
- class ValidateForeignKeyOnUsers < ActiveRecord::Migration[7.2]
499
+ class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.0]
496
500
  def change
497
501
  validate_foreign_key :users, :orders
498
502
  end
@@ -506,7 +510,7 @@ end
506
510
  In Postgres, adding a unique constraint creates a unique index, which blocks reads and writes.
507
511
 
508
512
  ```ruby
509
- class AddUniqueConstraint < ActiveRecord::Migration[7.2]
513
+ class AddUniqueConstraint < ActiveRecord::Migration[8.0]
510
514
  def change
511
515
  add_unique_constraint :users, :some_column
512
516
  end
@@ -518,7 +522,7 @@ end
518
522
  Create a unique index concurrently, then use it for the constraint.
519
523
 
520
524
  ```ruby
521
- class AddUniqueConstraint < ActiveRecord::Migration[7.2]
525
+ class AddUniqueConstraint < ActiveRecord::Migration[8.0]
522
526
  disable_ddl_transaction!
523
527
 
524
528
  def up
@@ -539,7 +543,7 @@ end
539
543
  In Postgres, adding an exclusion constraint blocks reads and writes while every row is checked.
540
544
 
541
545
  ```ruby
542
- class AddExclusionConstraint < ActiveRecord::Migration[7.2]
546
+ class AddExclusionConstraint < ActiveRecord::Migration[8.0]
543
547
  def change
544
548
  add_exclusion_constraint :users, "number WITH =", using: :gist
545
549
  end
@@ -557,7 +561,7 @@ end
557
561
  In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
558
562
 
559
563
  ```ruby
560
- class AddPropertiesToUsers < ActiveRecord::Migration[7.2]
564
+ class AddPropertiesToUsers < ActiveRecord::Migration[8.0]
561
565
  def change
562
566
  add_column :users, :properties, :json
563
567
  end
@@ -569,7 +573,7 @@ end
569
573
  Use `jsonb` instead.
570
574
 
571
575
  ```ruby
572
- class AddPropertiesToUsers < ActiveRecord::Migration[7.2]
576
+ class AddPropertiesToUsers < ActiveRecord::Migration[8.0]
573
577
  def change
574
578
  add_column :users, :properties, :jsonb
575
579
  end
@@ -585,7 +589,7 @@ end
585
589
  In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
586
590
 
587
591
  ```ruby
588
- class SetSomeColumnNotNull < ActiveRecord::Migration[7.2]
592
+ class SetSomeColumnNotNull < ActiveRecord::Migration[8.0]
589
593
  def change
590
594
  change_column_null :users, :some_column, false
591
595
  end
@@ -597,7 +601,7 @@ end
597
601
  Instead, add a check constraint.
598
602
 
599
603
  ```ruby
600
- class SetSomeColumnNotNull < ActiveRecord::Migration[7.2]
604
+ class SetSomeColumnNotNull < ActiveRecord::Migration[8.0]
601
605
  def change
602
606
  add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
603
607
  end
@@ -607,12 +611,17 @@ end
607
611
  Then validate it in a separate migration. Once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
608
612
 
609
613
  ```ruby
610
- class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.2]
611
- def change
614
+ class ValidateSomeColumnNotNull < ActiveRecord::Migration[8.0]
615
+ def up
612
616
  validate_check_constraint :users, name: "users_some_column_null"
613
617
  change_column_null :users, :some_column, false
614
618
  remove_check_constraint :users, name: "users_some_column_null"
615
619
  end
620
+
621
+ def down
622
+ add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
623
+ change_column_null :users, :some_column, true
624
+ end
616
625
  end
617
626
  ```
618
627
 
@@ -623,7 +632,7 @@ end
623
632
  Adding a column with a volatile default value to an existing table causes the entire table to be rewritten. During this time, reads and writes are blocked.
624
633
 
625
634
  ```ruby
626
- class AddSomeColumnToUsers < ActiveRecord::Migration[7.2]
635
+ class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
627
636
  def change
628
637
  add_column :users, :some_column, :uuid, default: "gen_random_uuid()"
629
638
  end
@@ -635,7 +644,7 @@ end
635
644
  Instead, add the column without a default value, then change the default.
636
645
 
637
646
  ```ruby
638
- class AddSomeColumnToUsers < ActiveRecord::Migration[7.2]
647
+ class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
639
648
  def up
640
649
  add_column :users, :some_column, :uuid
641
650
  change_column_default :users, :some_column, from: nil, to: "gen_random_uuid()"
@@ -686,7 +695,7 @@ config.active_record.partial_inserts = false
686
695
  Adding a non-unique index with more than three columns rarely improves performance.
687
696
 
688
697
  ```ruby
689
- class AddSomeIndexToUsers < ActiveRecord::Migration[7.2]
698
+ class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
690
699
  def change
691
700
  add_index :users, [:a, :b, :c, :d]
692
701
  end
@@ -698,7 +707,7 @@ end
698
707
  Instead, start an index with columns that narrow down the results the most.
699
708
 
700
709
  ```ruby
701
- class AddSomeIndexToUsers < ActiveRecord::Migration[7.2]
710
+ class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
702
711
  def change
703
712
  add_index :users, [:d, :b]
704
713
  end
@@ -712,7 +721,7 @@ For Postgres, be sure to add them concurrently.
712
721
  To mark a step in the migration as safe, despite using a method that might otherwise be dangerous, wrap it in a `safety_assured` block.
713
722
 
714
723
  ```ruby
715
- class MySafeMigration < ActiveRecord::Migration[7.2]
724
+ class MySafeMigration < ActiveRecord::Migration[8.0]
716
725
  def change
717
726
  safety_assured { remove_column :users, :some_column }
718
727
  end
@@ -723,7 +732,7 @@ Certain methods like `execute` and `change_table` cannot be inspected and are pr
723
732
 
724
733
  ## Safe by Default
725
734
 
726
- Make operations safe by default.
735
+ Make certain operations safe by default. This allows you to write the code under the "Bad" section, but the migration will be performed as if you had written the "Good" version.
727
736
 
728
737
  - adding and removing an index
729
738
  - adding a foreign key
@@ -772,6 +781,16 @@ StrongMigrations.disable_check(:add_index)
772
781
 
773
782
  Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
774
783
 
784
+ ## Skip Databases
785
+
786
+ Skip checks and other functionality for specific databases with:
787
+
788
+ ```ruby
789
+ StrongMigrations.skip_database(:catalog)
790
+ ```
791
+
792
+ Note: This does not affect `alphabetize_schema`.
793
+
775
794
  ## Down Migrations / Rollbacks
776
795
 
777
796
  By default, checks are disabled when migrating down. Enable them with:
@@ -853,7 +872,21 @@ production:
853
872
 
854
873
  For HTTP connections, Redis, and other services, check out [this guide](https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts).
855
874
 
856
- ## Lock Timeout Retries [experimental]
875
+ ## Invalid Indexes
876
+
877
+ In Postgres, adding an index non-concurrently can leave behind an invalid index if the lock timeout is reached. Running the migration again can result in an error.
878
+
879
+ To automatically remove the invalid index when the migration runs again, use:
880
+
881
+ ```ruby
882
+ StrongMigrations.remove_invalid_indexes = true
883
+ ```
884
+
885
+ Note: This feature is experimental.
886
+
887
+ ## Lock Timeout Retries
888
+
889
+ Note: This feature is experimental.
857
890
 
858
891
  There’s the option to automatically retry statements for migrations when the lock timeout is reached. Here’s how it works:
859
892
 
@@ -877,7 +910,7 @@ StrongMigrations.lock_timeout_retry_delay = 10.seconds
877
910
  To mark migrations as safe that were created before installing this gem, create an initializer with:
878
911
 
879
912
  ```ruby
880
- StrongMigrations.start_after = 20230101000000
913
+ StrongMigrations.start_after = 20250101000000
881
914
  ```
882
915
 
883
916
  Use the version from your latest migration.
@@ -13,11 +13,11 @@ module StrongMigrations
13
13
  end
14
14
 
15
15
  def set_statement_timeout(timeout)
16
- raise StrongMigrations::Error, "Statement timeout not supported for this database"
16
+ # do nothing
17
17
  end
18
18
 
19
19
  def set_lock_timeout(timeout)
20
- raise StrongMigrations::Error, "Lock timeout not supported for this database"
20
+ # do nothing
21
21
  end
22
22
 
23
23
  def check_lock_timeout(limit)
@@ -40,6 +40,9 @@ module StrongMigrations
40
40
  ["primary_key"]
41
41
  end
42
42
 
43
+ def max_constraint_name_length
44
+ end
45
+
43
46
  private
44
47
 
45
48
  def connection
@@ -92,6 +92,10 @@ module StrongMigrations
92
92
  "writes"
93
93
  end
94
94
 
95
+ def max_constraint_name_length
96
+ 64
97
+ end
98
+
95
99
  private
96
100
 
97
101
  # do not memoize
@@ -127,19 +127,19 @@ module StrongMigrations
127
127
  safe
128
128
  end
129
129
 
130
- def constraints(table_name)
130
+ # TODO remove when Active Record < 7.1 is no longer supported
131
+ def index_invalid?(table_name, index_name)
131
132
  query = <<~SQL
132
133
  SELECT
133
- conname AS name,
134
- pg_get_constraintdef(oid) AS def
134
+ indisvalid
135
135
  FROM
136
- pg_constraint
136
+ pg_index
137
137
  WHERE
138
- contype = 'c' AND
139
- convalidated AND
140
- conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
138
+ indrelid = to_regclass(#{connection.quote(connection.quote_table_name(table_name))}) AND
139
+ indexrelid = to_regclass(#{connection.quote(connection.quote_table_name(index_name))}) AND
140
+ indisvalid = false
141
141
  SQL
142
- select_all(query.squish).to_a
142
+ select_all(query.squish).any?
143
143
  end
144
144
 
145
145
  def writes_blocked?
@@ -173,6 +173,10 @@ module StrongMigrations
173
173
  ["primary_key", "serial", "bigserial"]
174
174
  end
175
175
 
176
+ def max_constraint_name_length
177
+ 63
178
+ end
179
+
176
180
  private
177
181
 
178
182
  def set_timeout(setting, timeout)
@@ -210,15 +214,9 @@ module StrongMigrations
210
214
  end
211
215
 
212
216
  def datetime_type
213
- key =
214
- if ActiveRecord::VERSION::MAJOR >= 7
215
- # https://github.com/rails/rails/pull/41084
216
- # no need to support custom datetime_types
217
- connection.class.datetime_type
218
- else
219
- # https://github.com/rails/rails/issues/21126#issuecomment-327895275
220
- :datetime
221
- end
217
+ # https://github.com/rails/rails/pull/41084
218
+ # no need to support custom datetime_types
219
+ key = connection.class.datetime_type
222
220
 
223
221
  # could be timestamp, timestamp without time zone, timestamp with time zone, etc
224
222
  connection.class.const_get(:NATIVE_DATABASE_TYPES).fetch(key).fetch(:name)
@@ -11,10 +11,16 @@ module StrongMigrations
11
11
 
12
12
  def initialize(migration)
13
13
  @migration = migration
14
+ reset
15
+ end
16
+
17
+ def reset
14
18
  @new_tables = []
15
19
  @new_columns = []
16
20
  @timeouts_set = false
17
21
  @committed = false
22
+ @transaction_disabled = false
23
+ @skip_retries = false
18
24
  end
19
25
 
20
26
  def self.safety_assured
@@ -27,7 +33,10 @@ module StrongMigrations
27
33
  end
28
34
  end
29
35
 
30
- def perform(method, *args)
36
+ def perform(method, *args, &block)
37
+ return yield if skip?
38
+
39
+ check_adapter
31
40
  check_version_supported
32
41
  set_timeouts
33
42
  check_lock_timeout
@@ -96,9 +105,9 @@ module StrongMigrations
96
105
  # TODO figure out how to handle methods that generate multiple statements
97
106
  # like add_reference(table, ref, index: {algorithm: :concurrently})
98
107
  # lock timeout after first statement will cause retry to fail
99
- retry_lock_timeouts { yield }
108
+ retry_lock_timeouts { perform_method(method, *args, &block) }
100
109
  else
101
- yield
110
+ perform_method(method, *args, &block)
102
111
  end
103
112
 
104
113
  # outdated statistics + a new index can hurt performance of existing queries
@@ -109,6 +118,13 @@ module StrongMigrations
109
118
  result
110
119
  end
111
120
 
121
+ def perform_method(method, *args)
122
+ if StrongMigrations.remove_invalid_indexes && direction == :up && method == :add_index && postgresql?
123
+ remove_invalid_index_if_needed(*args)
124
+ end
125
+ yield
126
+ end
127
+
112
128
  def retry_lock_timeouts(check_committed: false)
113
129
  retries = 0
114
130
  begin
@@ -129,8 +145,22 @@ module StrongMigrations
129
145
  version && version <= StrongMigrations.start_after
130
146
  end
131
147
 
148
+ def skip?
149
+ StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
150
+ end
151
+
132
152
  private
133
153
 
154
+ def check_adapter
155
+ return if defined?(@adapter_checked)
156
+
157
+ if adapter.instance_of?(Adapters::AbstractAdapter)
158
+ warn "[strong_migrations] Unsupported adapter: #{connection.adapter_name}. Use StrongMigrations.skip_database(#{db_config_name.to_sym.inspect}) to silence this warning."
159
+ end
160
+
161
+ @adapter_checked = true
162
+ end
163
+
134
164
  def check_version_supported
135
165
  return if defined?(@version_checked)
136
166
 
@@ -200,12 +230,48 @@ module StrongMigrations
200
230
  @migration.connection
201
231
  end
202
232
 
233
+ def db_config_name
234
+ connection.pool.db_config.name
235
+ end
236
+
203
237
  def retry_lock_timeouts?(method)
204
238
  (
205
239
  StrongMigrations.lock_timeout_retries > 0 &&
206
240
  !in_transaction? &&
207
- method != :transaction
241
+ method != :transaction &&
242
+ !@skip_retries
208
243
  )
209
244
  end
245
+
246
+ def without_retries
247
+ previous_value = @skip_retries
248
+ begin
249
+ @skip_retries = true
250
+ yield
251
+ ensure
252
+ @skip_retries = previous_value
253
+ end
254
+ end
255
+
256
+ # REINDEX INDEX CONCURRENTLY leaves a new invalid index if it fails, so use remove_index instead
257
+ def remove_invalid_index_if_needed(*args)
258
+ options = args.extract_options!
259
+
260
+ # ensures has same options as existing index
261
+ # check args to avoid errors with index_exists?
262
+ return unless args.size == 2 && connection.index_exists?(*args, **options.merge(valid: false))
263
+
264
+ table, columns = args
265
+ index_name = options.fetch(:name, connection.index_name(table, columns))
266
+
267
+ # valid option is ignored for Active Record < 7.1, so check name as well
268
+ return if ar_version < 7.1 && !adapter.index_invalid?(table, index_name)
269
+
270
+ @migration.say("Attempting to remove invalid index")
271
+ without_retries do
272
+ # TODO pass index schema for extra safety?
273
+ @migration.remove_index(table, **{name: index_name}.merge(options.slice(:algorithm)))
274
+ end
275
+ end
210
276
  end
211
277
  end
@@ -10,7 +10,7 @@ module StrongMigrations
10
10
  if !new_table?(table)
11
11
  if postgresql? && options[:validate] != false
12
12
  add_options = options.merge(validate: false)
13
- name = options[:name] || @migration.check_constraint_options(table, expression, options)[:name]
13
+ name = options[:name] || connection.check_constraint_options(table, expression, options)[:name]
14
14
  validate_options = {name: name}
15
15
 
16
16
  if StrongMigrations.safe_by_default
@@ -216,11 +216,11 @@ module StrongMigrations
216
216
  table, column, _default_or_changes = args
217
217
 
218
218
  # just check ActiveRecord::Base, even though can override on model
219
- partial_inserts = ar_version >= 7 ? ActiveRecord::Base.partial_inserts : ActiveRecord::Base.partial_writes
219
+ partial_inserts = ActiveRecord::Base.partial_inserts
220
220
 
221
221
  if partial_inserts && !new_column?(table, column)
222
222
  raise_error :change_column_default,
223
- config: ar_version >= 7 ? "partial_inserts" : "partial_writes"
223
+ config: "partial_inserts"
224
224
  end
225
225
  end
226
226
 
@@ -228,32 +228,40 @@ module StrongMigrations
228
228
  table, column, null, default = args
229
229
  if !null
230
230
  if postgresql?
231
- safe = adapter.constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" || c["def"] == "CHECK ((#{connection.quote_column_name(column)} IS NOT NULL))" }
231
+ constraints = connection.check_constraints(table)
232
+ safe = constraints.any? { |c| c.options[:validate] && (c.expression == "#{column} IS NOT NULL" || c.expression == "#{connection.quote_column_name(column)} IS NOT NULL") }
232
233
 
233
234
  unless safe
235
+ expression = "#{quote_column_if_needed(column)} IS NOT NULL"
236
+
234
237
  # match https://github.com/nullobject/rein
235
238
  constraint_name = "#{table}_#{column}_null"
239
+ if adapter.max_constraint_name_length && constraint_name.bytesize > adapter.max_constraint_name_length
240
+ constraint_name = connection.check_constraint_options(table, expression, {})[:name]
236
241
 
237
- add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
238
- validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
239
- remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
240
-
241
- validate_constraint_code = String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
242
+ # avoid collision with Active Record naming for safe_by_default
243
+ if StrongMigrations.safe_by_default
244
+ constraint_name = constraint_name.sub("rails", "strong_migrations")
245
+ end
246
+ end
242
247
 
248
+ add_args = [table, expression, {name: constraint_name, validate: false}]
249
+ validate_args = [table, {name: constraint_name}]
243
250
  change_args = [table, column, null]
244
-
245
- validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
246
- validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
251
+ remove_args = [table, {name: constraint_name}]
247
252
 
248
253
  if StrongMigrations.safe_by_default
249
- safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
254
+ safe_change_column_null(add_args, validate_args, change_args, remove_args, default, constraints)
250
255
  throw :safe
251
256
  end
252
257
 
253
- add_constraint_code = command_str(:add_check_constraint, [table, "#{quote_column_if_needed(column)} IS NOT NULL", {name: constraint_name, validate: false}])
258
+ add_constraint_code = command_str(:add_check_constraint, add_args)
254
259
 
260
+ up_code = String.new(command_str(:validate_check_constraint, validate_args))
261
+ up_code << "\n #{command_str(:change_column_null, change_args)}"
262
+ up_code << "\n #{command_str(:remove_check_constraint, remove_args)}"
255
263
  down_code = "#{add_constraint_code}\n #{command_str(:change_column_null, [table, column, true])}"
256
- validate_constraint_code = "def up\n #{validate_constraint_code}\n end\n\n def down\n #{down_code}\n end"
264
+ validate_constraint_code = "def up\n #{up_code}\n end\n\n def down\n #{down_code}\n end"
257
265
 
258
266
  raise_error :change_column_null_postgresql,
259
267
  add_constraint_code: add_constraint_code,
@@ -302,29 +310,28 @@ module StrongMigrations
302
310
  columns =
303
311
  case method
304
312
  when :remove_timestamps
305
- ["created_at", "updated_at"]
313
+ [:created_at, :updated_at]
306
314
  when :remove_column
307
- [args[1].to_s]
315
+ [args[1]]
308
316
  when :remove_columns
309
- # Active Record 6.1+ supports options
310
317
  if args.last.is_a?(Hash)
311
- args[1..-2].map(&:to_s)
318
+ args[1..-2]
312
319
  else
313
- args[1..-1].map(&:to_s)
320
+ args[1..-1]
314
321
  end
315
322
  else
316
323
  options = args[2] || {}
317
324
  reference = args[1]
318
325
  cols = []
319
- cols << "#{reference}_type" if options[:polymorphic]
320
- cols << "#{reference}_id"
326
+ cols << "#{reference}_type".to_sym if options[:polymorphic]
327
+ cols << "#{reference}_id".to_sym
321
328
  cols
322
329
  end
323
330
 
324
- code = "self.ignored_columns += #{columns.inspect}"
331
+ code = "self.ignored_columns += #{columns.map(&:to_s).inspect}"
325
332
 
326
333
  raise_error :remove_column,
327
- model: args[0].to_s.classify,
334
+ model: model_name(args[0]),
328
335
  code: code,
329
336
  command: command_str(method, args),
330
337
  column_suffix: columns.size > 1 ? "s" : ""
@@ -392,7 +399,7 @@ module StrongMigrations
392
399
  message = message + append if append
393
400
 
394
401
  vars[:migration_name] = @migration.class.name
395
- vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
402
+ vars[:migration_suffix] = migration_suffix
396
403
  vars[:base_model] = "ApplicationRecord"
397
404
 
398
405
  # escape % not followed by {
@@ -433,7 +440,7 @@ module StrongMigrations
433
440
  end
434
441
 
435
442
  def backfill_code(table, column, default, function = false)
436
- model = table.to_s.classify
443
+ model = model_name(table)
437
444
  if function
438
445
  # update_all(column: Arel.sql(default)) also works in newer versions of Active Record
439
446
  update_expr = "#{quote_column_if_needed(column)} = #{default}"
@@ -456,5 +463,13 @@ module StrongMigrations
456
463
  def new_column?(table, column)
457
464
  new_table?(table) || @new_columns.include?([table.to_s, column.to_s])
458
465
  end
466
+
467
+ def migration_suffix
468
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
469
+ end
470
+
471
+ def model_name(table)
472
+ table.to_s.classify
473
+ end
459
474
  end
460
475
  end
@@ -147,7 +147,7 @@ you're doing is safe before proceeding, then wrap it in a safety_assured { ... }
147
147
  create_table:
148
148
  "The force option will destroy existing tables.
149
149
  If this is intended, drop the existing table first.
150
- Otherwise, remove the force option.",
150
+ In any case, remove the force option.",
151
151
 
152
152
  execute:
153
153
  "Strong Migrations does not support inspecting what happens inside an
@@ -18,7 +18,8 @@ module StrongMigrations
18
18
  end
19
19
  end
20
20
  end
21
- ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
21
+ # same as ActiveRecord::Migration
22
+ ruby2_keywords(:method_missing)
22
23
 
23
24
  def revert(*)
24
25
  if strong_migrations_checker.version_safe?
@@ -1,8 +1,26 @@
1
1
  module StrongMigrations
2
- module DatabaseTasks
3
- def migrate(...)
2
+ module MigrationContext
3
+ def up(...)
4
4
  super
5
5
  rescue => e
6
+ strong_migrations_process_exception(e)
7
+ end
8
+
9
+ def down(...)
10
+ super
11
+ rescue => e
12
+ strong_migrations_process_exception(e)
13
+ end
14
+
15
+ def run(...)
16
+ super
17
+ rescue => e
18
+ strong_migrations_process_exception(e)
19
+ end
20
+
21
+ private
22
+
23
+ def strong_migrations_process_exception(e)
6
24
  if e.cause.is_a?(StrongMigrations::Error)
7
25
  # strip cause and clean backtrace
8
26
  def e.cause
@@ -6,11 +6,13 @@ module StrongMigrations
6
6
  # handle MigrationProxy class
7
7
  migration = migration.send(:migration) if migration.respond_to?(:migration, true)
8
8
 
9
- # retry migration since the entire transaction needs to be rerun
10
9
  checker = migration.send(:strong_migrations_checker)
10
+ return super if checker.skip?
11
+
12
+ # retry migration since the entire transaction needs to be rerun
11
13
  checker.retry_lock_timeouts(check_committed: true) do
12
14
  # failed transaction reverts timeout, so need to re-apply
13
- checker.timeouts_set = false
15
+ checker.reset
14
16
 
15
17
  super(migration, ...)
16
18
  end
@@ -4,7 +4,6 @@ module StrongMigrations
4
4
  StrongMigrations.safe_by_default && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :add_check_constraint, :change_column_null].include?(method)
5
5
  end
6
6
 
7
- # TODO check if invalid index with expected name exists and remove if needed
8
7
  def safe_add_index(*args, **options)
9
8
  disable_transaction
10
9
  @migration.add_index(*args, **options.merge(algorithm: :concurrently))
@@ -47,14 +46,16 @@ module StrongMigrations
47
46
  def safe_add_foreign_key(from_table, to_table, *args, **options)
48
47
  @migration.reversible do |dir|
49
48
  dir.up do
50
- @migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
49
+ # validate option is unintentionally ignored for Active Record < 7.1
50
+ # https://github.com/rails/rails/pull/45896
51
+ if !connection.foreign_key_exists?(from_table, to_table, **options.merge(validate: false))
52
+ @migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
53
+ end
51
54
  disable_transaction
52
- validate_options = options.slice(:column, :name)
53
- @migration.validate_foreign_key(from_table, to_table, **validate_options)
55
+ @migration.validate_foreign_key(from_table, to_table, **options.slice(:column, :name))
54
56
  end
55
57
  dir.down do
56
- remove_options = options.slice(:column, :name)
57
- @migration.remove_foreign_key(from_table, to_table, **remove_options)
58
+ @migration.remove_foreign_key(from_table, to_table, **options.slice(:column, :name))
58
59
  end
59
60
  end
60
61
  end
@@ -62,7 +63,10 @@ module StrongMigrations
62
63
  def safe_add_check_constraint(table, expression, *args, add_options, validate_options)
63
64
  @migration.reversible do |dir|
64
65
  dir.up do
65
- @migration.add_check_constraint(table, expression, *args, **add_options)
66
+ # only skip invalid constraints
67
+ unless connection.check_constraints(table).any? { |c| c.options[:name] == validate_options[:name] && !c.options[:validate] }
68
+ @migration.add_check_constraint(table, expression, *args, **add_options)
69
+ end
66
70
  disable_transaction
67
71
  @migration.validate_check_constraint(table, **validate_options)
68
72
  end
@@ -72,35 +76,33 @@ module StrongMigrations
72
76
  end
73
77
  end
74
78
 
75
- def safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
79
+ def safe_change_column_null(add_args, validate_args, change_args, remove_args, default, constraints)
76
80
  @migration.reversible do |dir|
77
81
  dir.up do
78
82
  unless default.nil?
79
83
  raise Error, "default value not supported yet with safe_by_default"
80
84
  end
81
85
 
82
- @migration.safety_assured do
83
- @migration.execute(add_code)
84
- disable_transaction
85
- @migration.execute(validate_code)
86
- end
87
- if change_args
88
- @migration.change_column_null(*change_args)
89
- @migration.safety_assured do
90
- @migration.execute(remove_code)
91
- end
86
+ add_options = add_args.extract_options!
87
+ validate_options = validate_args.extract_options!
88
+ remove_options = remove_args.extract_options!
89
+
90
+ # only skip invalid constraints
91
+ unless constraints.any? { |c| c.options[:name] == validate_options[:name] && !c.options[:validate] }
92
+ @migration.add_check_constraint(*add_args, **add_options)
92
93
  end
94
+ disable_transaction
95
+
96
+ connection.begin_db_transaction
97
+ @migration.validate_check_constraint(*validate_args, **validate_options)
98
+ @migration.change_column_null(*change_args)
99
+ @migration.remove_check_constraint(*remove_args, **remove_options)
100
+ connection.commit_db_transaction
93
101
  end
94
102
  dir.down do
95
- if change_args
96
- down_args = change_args.dup
97
- down_args[2] = true
98
- @migration.change_column_null(*down_args)
99
- else
100
- @migration.safety_assured do
101
- @migration.execute(remove_code)
102
- end
103
- end
103
+ down_args = change_args.dup
104
+ down_args[2] = true
105
+ @migration.change_column_null(*down_args)
104
106
  end
105
107
  end
106
108
  end
@@ -109,13 +111,13 @@ module StrongMigrations
109
111
  # so just commit at start
110
112
  def disable_transaction
111
113
  if in_transaction? && !transaction_disabled
112
- @migration.connection.commit_db_transaction
114
+ connection.commit_db_transaction
113
115
  self.transaction_disabled = true
114
116
  end
115
117
  end
116
118
 
117
119
  def in_transaction?
118
- @migration.connection.open_transactions > 0
120
+ connection.open_transactions > 0
119
121
  end
120
122
  end
121
123
  end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "2.0.2"
2
+ VERSION = "2.2.0"
3
3
  end
@@ -11,8 +11,8 @@ require_relative "strong_migrations/adapters/postgresql_adapter"
11
11
  require_relative "strong_migrations/checks"
12
12
  require_relative "strong_migrations/safe_methods"
13
13
  require_relative "strong_migrations/checker"
14
- require_relative "strong_migrations/database_tasks"
15
14
  require_relative "strong_migrations/migration"
15
+ require_relative "strong_migrations/migration_context"
16
16
  require_relative "strong_migrations/migrator"
17
17
  require_relative "strong_migrations/version"
18
18
 
@@ -29,7 +29,7 @@ module StrongMigrations
29
29
  :target_postgresql_version, :target_mysql_version, :target_mariadb_version,
30
30
  :enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version,
31
31
  :safe_by_default, :target_sql_mode, :lock_timeout_retries, :lock_timeout_retry_delay,
32
- :alphabetize_schema
32
+ :alphabetize_schema, :skipped_databases, :remove_invalid_indexes
33
33
  attr_writer :lock_timeout_limit
34
34
  end
35
35
  self.auto_analyze = false
@@ -40,6 +40,8 @@ module StrongMigrations
40
40
  self.safe_by_default = false
41
41
  self.check_down = false
42
42
  self.alphabetize_schema = false
43
+ self.skipped_databases = []
44
+ self.remove_invalid_indexes = false
43
45
 
44
46
  # private
45
47
  def self.developer_env?
@@ -83,6 +85,10 @@ module StrongMigrations
83
85
  false
84
86
  end
85
87
  end
88
+
89
+ def self.skip_database(database)
90
+ self.skipped_databases << database
91
+ end
86
92
  end
87
93
 
88
94
  # load error messages
@@ -90,12 +96,9 @@ require_relative "strong_migrations/error_messages"
90
96
 
91
97
  ActiveSupport.on_load(:active_record) do
92
98
  ActiveRecord::Migration.prepend(StrongMigrations::Migration)
99
+ ActiveRecord::MigrationContext.prepend(StrongMigrations::MigrationContext)
93
100
  ActiveRecord::Migrator.prepend(StrongMigrations::Migrator)
94
101
 
95
- if defined?(ActiveRecord::Tasks::DatabaseTasks)
96
- ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(StrongMigrations::DatabaseTasks)
97
- end
98
-
99
102
  require_relative "strong_migrations/schema_dumper"
100
103
  ActiveRecord::SchemaDumper.prepend(StrongMigrations::SchemaDumper)
101
104
  end
metadata CHANGED
@@ -1,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strong_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  - Bob Remeika
9
9
  - David Waller
10
- autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2024-10-30 00:00:00.000000000 Z
12
+ date: 2025-02-01 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: activerecord
@@ -18,15 +17,14 @@ dependencies:
18
17
  requirements:
19
18
  - - ">="
20
19
  - !ruby/object:Gem::Version
21
- version: '6.1'
20
+ version: '7'
22
21
  type: :runtime
23
22
  prerelease: false
24
23
  version_requirements: !ruby/object:Gem::Requirement
25
24
  requirements:
26
25
  - - ">="
27
26
  - !ruby/object:Gem::Version
28
- version: '6.1'
29
- description:
27
+ version: '7'
30
28
  email:
31
29
  - andrew@ankane.org
32
30
  - bob.remeika@gmail.com
@@ -47,9 +45,9 @@ files:
47
45
  - lib/strong_migrations/adapters/postgresql_adapter.rb
48
46
  - lib/strong_migrations/checker.rb
49
47
  - lib/strong_migrations/checks.rb
50
- - lib/strong_migrations/database_tasks.rb
51
48
  - lib/strong_migrations/error_messages.rb
52
49
  - lib/strong_migrations/migration.rb
50
+ - lib/strong_migrations/migration_context.rb
53
51
  - lib/strong_migrations/migrator.rb
54
52
  - lib/strong_migrations/railtie.rb
55
53
  - lib/strong_migrations/safe_methods.rb
@@ -60,7 +58,6 @@ homepage: https://github.com/ankane/strong_migrations
60
58
  licenses:
61
59
  - MIT
62
60
  metadata: {}
63
- post_install_message:
64
61
  rdoc_options: []
65
62
  require_paths:
66
63
  - lib
@@ -75,8 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
72
  - !ruby/object:Gem::Version
76
73
  version: '0'
77
74
  requirements: []
78
- rubygems_version: 3.5.16
79
- signing_key:
75
+ rubygems_version: 3.6.2
80
76
  specification_version: 4
81
77
  summary: Catch unsafe migrations in development
82
78
  test_files: []