strong_migrations 0.7.2 → 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +23 -1
- data/lib/strong_migrations.rb +4 -1
- data/lib/strong_migrations/checker.rb +33 -11
- data/lib/strong_migrations/migration.rb +1 -0
- data/lib/strong_migrations/safe_methods.rb +112 -0
- data/lib/strong_migrations/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '008201c52e7ae0bc2f0614b7b820b2dcb4c99c2ba1d270bd1cd4876bfa7c0258'
|
4
|
+
data.tar.gz: 19907e788d2b789ed04f49cdbaac2db7c6ddfe42315dbe53a440514d4b419019
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0231f799c80dc8dedbca766d45cc465d569d872ed2744c2348d31cac279d506e8118aacd5bb478833c7f10c427dc9ceff5c0e8aefe815090dbfbd927aaf0e13c
|
7
|
+
data.tar.gz: ecabbcb9bf64a5e9709f0918497ea31cd36829e06db4a8bec375a87cd63faf6ffc3efc2c33c9ca0f5e524f5d397d01aac41bd31fe36e0af47dd0996c4cb75097
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -8,7 +8,7 @@ Supports for PostgreSQL, MySQL, and MariaDB
|
|
8
8
|
|
9
9
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
10
10
|
|
11
|
-
[![Build Status](https://
|
11
|
+
[![Build Status](https://github.com/ankane/strong_migrations/workflows/build/badge.svg?branch=master)](https://github.com/ankane/strong_migrations/actions)
|
12
12
|
|
13
13
|
## Installation
|
14
14
|
|
@@ -311,6 +311,8 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
311
311
|
|
312
312
|
### Setting NOT NULL on an existing column
|
313
313
|
|
314
|
+
:turtle: Safe by default available
|
315
|
+
|
314
316
|
#### Bad
|
315
317
|
|
316
318
|
Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
@@ -373,6 +375,8 @@ end
|
|
373
375
|
|
374
376
|
### Adding an index non-concurrently
|
375
377
|
|
378
|
+
:turtle: Safe by default available
|
379
|
+
|
376
380
|
#### Bad
|
377
381
|
|
378
382
|
In Postgres, adding an index non-concurrently blocks writes.
|
@@ -409,6 +413,8 @@ rails g index table column
|
|
409
413
|
|
410
414
|
### Adding a reference
|
411
415
|
|
416
|
+
:turtle: Safe by default available
|
417
|
+
|
412
418
|
#### Bad
|
413
419
|
|
414
420
|
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
@@ -437,6 +443,8 @@ end
|
|
437
443
|
|
438
444
|
### Adding a foreign key
|
439
445
|
|
446
|
+
:turtle: Safe by default available
|
447
|
+
|
440
448
|
#### Bad
|
441
449
|
|
442
450
|
In Postgres, adding a foreign key blocks writes on both tables.
|
@@ -575,6 +583,20 @@ end
|
|
575
583
|
|
576
584
|
Certain methods like `execute` and `change_table` cannot be inspected and are prevented from running by default. Make sure what you’re doing is really safe and use this pattern.
|
577
585
|
|
586
|
+
## Safe by Default [experimental]
|
587
|
+
|
588
|
+
Make operations safe by default.
|
589
|
+
|
590
|
+
- adding and removing an index
|
591
|
+
- adding a foreign key
|
592
|
+
- setting NOT NULL on an existing column
|
593
|
+
|
594
|
+
Add to `config/initializers/strong_migrations.rb`:
|
595
|
+
|
596
|
+
```ruby
|
597
|
+
StrongMigrations.safe_by_default = true
|
598
|
+
```
|
599
|
+
|
578
600
|
## Custom Checks
|
579
601
|
|
580
602
|
Add your own custom checks with:
|
data/lib/strong_migrations.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
require "active_support"
|
3
3
|
|
4
4
|
# modules
|
5
|
+
require "strong_migrations/safe_methods"
|
5
6
|
require "strong_migrations/checker"
|
6
7
|
require "strong_migrations/database_tasks"
|
7
8
|
require "strong_migrations/migration"
|
@@ -17,12 +18,14 @@ module StrongMigrations
|
|
17
18
|
class << self
|
18
19
|
attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
|
19
20
|
:target_postgresql_version, :target_mysql_version, :target_mariadb_version,
|
20
|
-
:enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version
|
21
|
+
:enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version,
|
22
|
+
:safe_by_default
|
21
23
|
attr_writer :lock_timeout_limit
|
22
24
|
end
|
23
25
|
self.auto_analyze = false
|
24
26
|
self.start_after = 0
|
25
27
|
self.checks = []
|
28
|
+
self.safe_by_default = false
|
26
29
|
self.error_messages = {
|
27
30
|
add_column_default:
|
28
31
|
"Adding a column with a non-null default blocks %{rewrite_blocks} while the entire table is rewritten.
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module StrongMigrations
|
2
2
|
class Checker
|
3
|
-
|
3
|
+
include SafeMethods
|
4
|
+
|
5
|
+
attr_accessor :direction, :transaction_disabled
|
4
6
|
|
5
7
|
def initialize(migration)
|
6
8
|
@migration = migration
|
@@ -24,7 +26,7 @@ module StrongMigrations
|
|
24
26
|
set_timeouts
|
25
27
|
check_lock_timeout
|
26
28
|
|
27
|
-
|
29
|
+
if !safe? || safe_by_default_method?(method)
|
28
30
|
case method
|
29
31
|
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
|
30
32
|
columns =
|
@@ -65,6 +67,7 @@ module StrongMigrations
|
|
65
67
|
raise_error :add_index_columns, header: "Best practice"
|
66
68
|
end
|
67
69
|
if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
|
70
|
+
return safe_add_index(table, columns, options) if StrongMigrations.safe_by_default
|
68
71
|
raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
|
69
72
|
end
|
70
73
|
when :remove_index
|
@@ -75,6 +78,7 @@ module StrongMigrations
|
|
75
78
|
options ||= {}
|
76
79
|
|
77
80
|
if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
|
81
|
+
return safe_remove_index(table, options) if StrongMigrations.safe_by_default
|
78
82
|
raise_error :remove_index, command: command_str("remove_index", [table, options.merge(algorithm: :concurrently)])
|
79
83
|
end
|
80
84
|
when :add_column
|
@@ -184,14 +188,14 @@ Then add the NOT NULL constraint in separate migrations."
|
|
184
188
|
bad_index = index_value && !concurrently_set
|
185
189
|
|
186
190
|
if bad_index || options[:foreign_key]
|
187
|
-
columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
|
188
|
-
|
189
191
|
if index_value.is_a?(Hash)
|
190
192
|
options[:index] = options[:index].merge(algorithm: :concurrently)
|
191
193
|
else
|
192
194
|
options = options.merge(index: {algorithm: :concurrently})
|
193
195
|
end
|
194
196
|
|
197
|
+
return safe_add_reference(table, reference, options) if StrongMigrations.safe_by_default
|
198
|
+
|
195
199
|
if options.delete(:foreign_key)
|
196
200
|
headline = "Adding a foreign key blocks writes on both tables."
|
197
201
|
append = "
|
@@ -223,14 +227,22 @@ Then add the foreign key in separate migrations."
|
|
223
227
|
# match https://github.com/nullobject/rein
|
224
228
|
constraint_name = "#{table}_#{column}_null"
|
225
229
|
|
226
|
-
|
230
|
+
add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
|
231
|
+
validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
|
232
|
+
remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
|
233
|
+
|
234
|
+
validate_constraint_code = String.new(safety_assured_str(validate_code))
|
227
235
|
if postgresql_version >= Gem::Version.new("12")
|
228
|
-
|
229
|
-
|
236
|
+
change_args = [table, column, null]
|
237
|
+
|
238
|
+
validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
|
239
|
+
validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
|
230
240
|
end
|
231
241
|
|
242
|
+
return safe_change_column_null(add_code, validate_code, change_args, remove_code) if StrongMigrations.safe_by_default
|
243
|
+
|
232
244
|
raise_error :change_column_null_postgresql,
|
233
|
-
add_constraint_code:
|
245
|
+
add_constraint_code: safety_assured_str(add_code),
|
234
246
|
validate_constraint_code: validate_constraint_code
|
235
247
|
end
|
236
248
|
elsif mysql? || mariadb?
|
@@ -255,10 +267,17 @@ Then add the foreign key in separate migrations."
|
|
255
267
|
hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10)
|
256
268
|
fk_name = options[:name] || "fk_rails_#{hashed_identifier}"
|
257
269
|
|
270
|
+
add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key])
|
271
|
+
validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
|
272
|
+
|
273
|
+
return safe_add_foreign_key_code(from_table, to_table, add_code, validate_code) if StrongMigrations.safe_by_default
|
274
|
+
|
258
275
|
raise_error :add_foreign_key,
|
259
|
-
add_foreign_key_code:
|
260
|
-
validate_foreign_key_code:
|
276
|
+
add_foreign_key_code: safety_assured_str(add_code),
|
277
|
+
validate_foreign_key_code: safety_assured_str(validate_code)
|
261
278
|
else
|
279
|
+
return safe_add_foreign_key(from_table, to_table, options) if StrongMigrations.safe_by_default
|
280
|
+
|
262
281
|
raise_error :add_foreign_key,
|
263
282
|
add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
|
264
283
|
validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
|
@@ -475,7 +494,10 @@ Then add the foreign key in separate migrations."
|
|
475
494
|
|
476
495
|
def constraint_str(statement, identifiers)
|
477
496
|
# not all identifiers are tables, but this method of quoting should be fine
|
478
|
-
|
497
|
+
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
498
|
+
end
|
499
|
+
|
500
|
+
def safety_assured_str(code)
|
479
501
|
"safety_assured do\n execute '#{code}' \n end"
|
480
502
|
end
|
481
503
|
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module StrongMigrations
|
2
|
+
module SafeMethods
|
3
|
+
def safe_by_default_method?(method)
|
4
|
+
StrongMigrations.safe_by_default && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :change_column_null].include?(method)
|
5
|
+
end
|
6
|
+
|
7
|
+
# TODO check if invalid index with expected name exists and remove if needed
|
8
|
+
def safe_add_index(table, columns, options)
|
9
|
+
disable_transaction
|
10
|
+
@migration.add_index(table, columns, options.merge(algorithm: :concurrently))
|
11
|
+
end
|
12
|
+
|
13
|
+
def safe_remove_index(table, options)
|
14
|
+
disable_transaction
|
15
|
+
@migration.remove_index(table, options.merge(algorithm: :concurrently))
|
16
|
+
end
|
17
|
+
|
18
|
+
def safe_add_reference(table, reference, options)
|
19
|
+
@migration.reversible do |dir|
|
20
|
+
dir.up do
|
21
|
+
disable_transaction
|
22
|
+
foreign_key = options.delete(:foreign_key)
|
23
|
+
@migration.add_reference(table, reference, **options)
|
24
|
+
if foreign_key
|
25
|
+
# same as Active Record
|
26
|
+
name =
|
27
|
+
if foreign_key.is_a?(Hash) && foreign_key[:to_table]
|
28
|
+
foreign_key[:to_table]
|
29
|
+
else
|
30
|
+
(ActiveRecord::Base.pluralize_table_names ? reference.to_s.pluralize : reference).to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
@migration.add_foreign_key(table, name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
dir.down do
|
37
|
+
@migration.remove_reference(table, reference)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def safe_add_foreign_key(from_table, to_table, options)
|
43
|
+
@migration.reversible do |dir|
|
44
|
+
dir.up do
|
45
|
+
@migration.add_foreign_key(from_table, to_table, **options.merge(validate: false))
|
46
|
+
disable_transaction
|
47
|
+
@migration.validate_foreign_key(from_table, to_table)
|
48
|
+
end
|
49
|
+
dir.down do
|
50
|
+
@migration.remove_foreign_key(from_table, to_table)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def safe_add_foreign_key_code(from_table, to_table, add_code, validate_code)
|
56
|
+
@migration.reversible do |dir|
|
57
|
+
dir.up do
|
58
|
+
@migration.safety_assured do
|
59
|
+
@migration.execute(add_code)
|
60
|
+
disable_transaction
|
61
|
+
@migration.execute(validate_code)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
dir.down do
|
65
|
+
@migration.remove_foreign_key(from_table, to_table)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def safe_change_column_null(add_code, validate_code, change_args, remove_code)
|
71
|
+
@migration.reversible do |dir|
|
72
|
+
dir.up do
|
73
|
+
@migration.safety_assured do
|
74
|
+
@migration.execute(add_code)
|
75
|
+
disable_transaction
|
76
|
+
@migration.execute(validate_code)
|
77
|
+
end
|
78
|
+
if change_args
|
79
|
+
@migration.change_column_null(*change_args)
|
80
|
+
@migration.safety_assured do
|
81
|
+
@migration.execute(remove_code)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
dir.down do
|
86
|
+
if change_args
|
87
|
+
down_args = change_args.dup
|
88
|
+
down_args[2] = true
|
89
|
+
@migration.change_column_null(*down_args)
|
90
|
+
else
|
91
|
+
@migration.safety_assured do
|
92
|
+
@migration.execute(remove_code)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# hard to commit at right time when reverting
|
100
|
+
# so just commit at start
|
101
|
+
def disable_transaction
|
102
|
+
if in_transaction? && !transaction_disabled
|
103
|
+
@migration.connection.commit_db_transaction
|
104
|
+
self.transaction_disabled = true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def in_transaction?
|
109
|
+
@migration.connection.open_transactions > 0
|
110
|
+
end
|
111
|
+
end
|
112
|
+
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: 0.7.
|
4
|
+
version: 0.7.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2020-
|
13
|
+
date: 2020-11-24 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -116,6 +116,7 @@ files:
|
|
116
116
|
- lib/strong_migrations/database_tasks.rb
|
117
117
|
- lib/strong_migrations/migration.rb
|
118
118
|
- lib/strong_migrations/railtie.rb
|
119
|
+
- lib/strong_migrations/safe_methods.rb
|
119
120
|
- lib/strong_migrations/version.rb
|
120
121
|
- lib/tasks/strong_migrations.rake
|
121
122
|
homepage: https://github.com/ankane/strong_migrations
|