strong_migrations 2.5.2 → 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: 9c7db529b15ac630f2f6c9e284b185ca5affbdb275c2d34d26e508317f6e2fa4
4
- data.tar.gz: 8b00a79a0f4cb787d43de827cbafe495c081b5250db4f08d75a893953778d72a
3
+ metadata.gz: 33d90fe56d07dcdf864739aeae07dac0c63b6504460299724bc293a43c5aeb7e
4
+ data.tar.gz: 9e7ea3be4a0d3b8b03bcb2e4e6b5f6b4425057388a4b8ae52a7263d5497d5de8
5
5
  SHA512:
6
- metadata.gz: 73091c6f13637427c09ed2dc013f804ac1ce91d78844b6dc10b7deb89aeb4d391123c4f16e661b34a1fd9e8013f5c6c50295e2a45824ca042e9418d3c6cd338d
7
- data.tar.gz: 96bbd494942e1a9f3a529f2ee7453fb693e9ec61aba22846d6be9c5a986b9213053142f9f06a87dc16dbc9361520ae9ae84e32c72e43654fdd0f2da921590cdb
6
+ metadata.gz: fe975030ddbdd8bce5f8bc1a18498e67e505627618a47d201e916d6c0ca21f846852ef08dc923e3a8626c2634fdcfa4d38fe9b674bc13b58d7a0c5064a012f0a
7
+ data.tar.gz: 4e0fe03e8ac9cb3872a7d9179f5902a8044587ffab5b22c02c7bbf63303a9bee48e195c6b77f30491f382ff7435a22cfe9e92c0a6dc53f145cf67d1f21cfb109
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ## 2.5.2 (2025-12-20)
2
8
 
3
9
  - 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
@@ -82,6 +82,11 @@ 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
+ 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
+
85
90
  Best practices:
86
91
 
87
92
  - [keeping non-unique indexes to three columns or less](#keeping-non-unique-indexes-to-three-columns-or-less)
@@ -146,14 +151,14 @@ Type | Safe Changes
146
151
  --- | ---
147
152
  `cidr` | Changing to `inet`
148
153
  `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+
154
+ `datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC
150
155
  `decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
151
156
  `interval` | Increasing or removing `:precision`
152
157
  `numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
153
158
  `string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
154
159
  `text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
155
160
  `time` | Increasing or removing `:precision`
156
- `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
157
162
 
158
163
  And some in MySQL and MariaDB:
159
164
 
@@ -680,6 +685,58 @@ A safer approach is to:
680
685
  5. Stop writing to the old schema
681
686
  6. Drop the old schema
682
687
 
688
+ ### Using the COPY algorithm
689
+
690
+ #### Bad
691
+
692
+ In MySQL and MariaDB, using the `COPY` algorithm blocks writes.
693
+
694
+ ```ruby
695
+ class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
696
+ def change
697
+ add_index :users, :some_column, algorithm: :copy
698
+ end
699
+ end
700
+ ```
701
+
702
+ #### Good
703
+
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.
719
+
720
+ ```ruby
721
+ class AddSomeIndexToUsers < ActiveRecord::Migration[8.2]
722
+ def change
723
+ add_index :users, :some_column, lock: :shared
724
+ end
725
+ end
726
+ ```
727
+
728
+ #### Good
729
+
730
+ Use the default locking or no locking.
731
+
732
+ ```ruby
733
+ class AddSomeIndexToUsers < ActiveRecord::Migration[8.2]
734
+ def change
735
+ add_index :users, :some_column
736
+ end
737
+ end
738
+ ```
739
+
683
740
  ### Keeping non-unique indexes to three columns or less
684
741
 
685
742
  #### Bad
@@ -910,7 +967,7 @@ Use the version from your latest migration.
910
967
  If your development database version is different from production, you can specify the production version so the right checks run in development.
911
968
 
912
969
  ```ruby
913
- StrongMigrations.target_version = 10 # or 8.0, 10.5, etc
970
+ StrongMigrations.target_version = 16
914
971
  ```
915
972
 
916
973
  The major version works well for Postgres, while the major and minor version is recommended for MySQL and MariaDB.
@@ -920,7 +977,7 @@ For safety, this option only affects development and test environments. In other
920
977
  If your app has multiple databases with different versions, you can use:
921
978
 
922
979
  ```ruby
923
- StrongMigrations.target_version = {primary: 13, catalog: 15}
980
+ StrongMigrations.target_version = {primary: 16, catalog: 18}
924
981
  ```
925
982
 
926
983
  ## 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
 
@@ -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)
@@ -155,7 +164,7 @@ module StrongMigrations
155
164
 
156
165
  if index_unsafe || foreign_key_unsafe
157
166
  if index_value.is_a?(Hash)
158
- options[:index] = options[:index].merge(algorithm: :concurrently)
167
+ options = options.merge(index: index_value.merge(algorithm: :concurrently))
159
168
  elsif index_value
160
169
  options = options.merge(index: {algorithm: :concurrently})
161
170
  end
@@ -179,6 +188,41 @@ module StrongMigrations
179
188
  append: append
180
189
  end
181
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
182
226
  end
183
227
 
184
228
  def check_add_unique_constraint(*args)
@@ -190,9 +234,9 @@ module StrongMigrations
190
234
  if column && !new_table?(table)
191
235
  index_name = connection.index_name(table, {column: column})
192
236
  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])
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])
196
240
  end
197
241
  end
198
242
 
@@ -224,16 +268,16 @@ module StrongMigrations
224
268
  if constraints.any?
225
269
  change_commands = []
226
270
  constraints.each do |c|
227
- 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}])
228
272
  end
229
- change_commands << command_str(:change_column, args + [options])
273
+ change_commands << command_str("change_column", args + [options])
230
274
  constraints.each do |c|
231
- 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}])
232
276
  end
233
277
 
234
278
  validate_commands = []
235
279
  constraints.each do |c|
236
- validate_commands << command_str(:validate_check_constraint, [table, {name: c.name}])
280
+ validate_commands << command_str("validate_check_constraint", [table, {name: c.name}])
237
281
  end
238
282
 
239
283
  raise_error :change_column_constraint,
@@ -241,6 +285,11 @@ module StrongMigrations
241
285
  validate_constraint_code: validate_commands.join("\n ")
242
286
  end
243
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)
244
293
  end
245
294
 
246
295
  def check_change_column_default(*args)
@@ -286,12 +335,12 @@ module StrongMigrations
286
335
  throw :safe
287
336
  end
288
337
 
289
- add_constraint_code = command_str(:add_check_constraint, add_args)
338
+ add_constraint_code = command_str("add_check_constraint", add_args)
290
339
 
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])}"
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])}"
295
344
  validate_constraint_code = "def up\n #{up_code}\n end\n\n def down\n #{down_code}\n end"
296
345
 
297
346
  raise_error :change_column_null_postgresql,
@@ -337,6 +386,7 @@ module StrongMigrations
337
386
  raise_error :execute, header: "Possibly dangerous operation"
338
387
  end
339
388
 
389
+ # supports algorithm and lock options, but always raises
340
390
  def check_remove_column(method, *args)
341
391
  columns =
342
392
  case method
@@ -383,8 +433,14 @@ module StrongMigrations
383
433
 
384
434
  raise_error :remove_index, command: command_str("remove_index", args + [options.merge(algorithm: :concurrently)])
385
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)
386
441
  end
387
442
 
443
+ # supports algorithm and lock options, but always raises
388
444
  def check_rename_column
389
445
  raise_error :rename_column
390
446
  end
@@ -442,6 +498,21 @@ module StrongMigrations
442
498
  @migration.stop!(message, header: header || "Dangerous operation detected")
443
499
  end
444
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
+
445
516
  def constraint_str(statement, identifiers)
446
517
  # not all identifiers are tables, but this method of quoting should be fine
447
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.2"
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.2
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: []