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 +4 -4
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +1 -1
- data/README.md +61 -4
- data/lib/generators/strong_migrations/install_generator.rb +2 -2
- data/lib/strong_migrations/checks.rb +84 -13
- data/lib/strong_migrations/error_messages.rb +18 -0
- data/lib/strong_migrations/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33d90fe56d07dcdf864739aeae07dac0c63b6504460299724bc293a43c5aeb7e
|
|
4
|
+
data.tar.gz: 9e7ea3be4a0d3b8b03bcb2e4e6b5f6b4425057388a4b8ae52a7263d5497d5de8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
|
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
|
|
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 =
|
|
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:
|
|
980
|
+
StrongMigrations.target_version = {primary: 16, catalog: 18}
|
|
924
981
|
```
|
|
925
982
|
|
|
926
983
|
## Analyze Tables
|
|
@@ -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
|
|
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(
|
|
194
|
-
constraint_command: command_str(
|
|
195
|
-
remove_command: command_str(
|
|
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(
|
|
271
|
+
change_commands << command_str("remove_check_constraint", [table, c.expression, {name: c.name}])
|
|
228
272
|
end
|
|
229
|
-
change_commands << command_str(
|
|
273
|
+
change_commands << command_str("change_column", args + [options])
|
|
230
274
|
constraints.each do |c|
|
|
231
|
-
change_commands << command_str(
|
|
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(
|
|
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(
|
|
338
|
+
add_constraint_code = command_str("add_check_constraint", add_args)
|
|
290
339
|
|
|
291
|
-
up_code = String.new(command_str(
|
|
292
|
-
up_code << "\n #{command_str(
|
|
293
|
-
up_code << "\n #{command_str(
|
|
294
|
-
down_code = "#{add_constraint_code}\n #{command_str(
|
|
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
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: strong_migrations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.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.
|
|
20
|
+
version: '7.2'
|
|
21
21
|
type: :runtime
|
|
22
22
|
prerelease: false
|
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
|
24
24
|
requirements:
|
|
25
25
|
- - ">="
|
|
26
26
|
- !ruby/object:Gem::Version
|
|
27
|
-
version: '7.
|
|
27
|
+
version: '7.2'
|
|
28
28
|
email:
|
|
29
29
|
- andrew@ankane.org
|
|
30
30
|
- bob.remeika@gmail.com
|
|
@@ -65,14 +65,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
65
65
|
requirements:
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '3.
|
|
68
|
+
version: '3.3'
|
|
69
69
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
70
|
requirements:
|
|
71
71
|
- - ">="
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
73
|
version: '0'
|
|
74
74
|
requirements: []
|
|
75
|
-
rubygems_version:
|
|
75
|
+
rubygems_version: 4.0.6
|
|
76
76
|
specification_version: 4
|
|
77
77
|
summary: Catch unsafe migrations in development
|
|
78
78
|
test_files: []
|