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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3410e3379861436cbec2b46796069db388fffea222fa50ff46b054a153fbdaf
4
- data.tar.gz: f328bee4412388114e888557d9b691ed3dad10bd0a5bf1ffb93c09f0b48d6696
3
+ metadata.gz: '008201c52e7ae0bc2f0614b7b820b2dcb4c99c2ba1d270bd1cd4876bfa7c0258'
4
+ data.tar.gz: 19907e788d2b789ed04f49cdbaac2db7c6ddfe42315dbe53a440514d4b419019
5
5
  SHA512:
6
- metadata.gz: 7e588f76685aa486f38949629b2f0aebe5f1acf92a5b53a29b4e4e1f5f334439eec3ef915240736caf55cff1f21197369305ec073c6adbbedaf6477aabb282cb
7
- data.tar.gz: 3e350c8a68ec076295d36aac330ff9eae5ad75329b9775fb8839319f7fbc99c62b8ea58ca5fc57ec36c750a510f9ae743a76fd6fedc7fd755d1e07b7ab7230b5
6
+ metadata.gz: 0231f799c80dc8dedbca766d45cc465d569d872ed2744c2348d31cac279d506e8118aacd5bb478833c7f10c427dc9ceff5c0e8aefe815090dbfbd927aaf0e13c
7
+ data.tar.gz: ecabbcb9bf64a5e9709f0918497ea31cd36829e06db4a8bec375a87cd63faf6ffc3efc2c33c9ca0f5e524f5d397d01aac41bd31fe36e0af47dd0996c4cb75097
@@ -1,3 +1,7 @@
1
+ ## 0.7.3 (2020-11-24)
2
+
3
+ - Added `safe_by_default` option
4
+
1
5
  ## 0.7.2 (2020-10-25)
2
6
 
3
7
  - Added support for float timeouts
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://travis-ci.org/ankane/strong_migrations.svg?branch=master)](https://travis-ci.org/ankane/strong_migrations)
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:
@@ -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
- attr_accessor :direction
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
- unless safe?
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
- validate_constraint_code = String.new(constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name]))
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
- validate_constraint_code << "\n #{command_str(:change_column_null, [table, column, null])}"
229
- validate_constraint_code << "\n #{constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])}"
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: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
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: 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]),
260
- validate_foreign_key_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
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
- code = statement % identifiers.map { |v| connection.quote_table_name(v) }
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
 
@@ -3,6 +3,7 @@ module StrongMigrations
3
3
  def migrate(direction)
4
4
  strong_migrations_checker.direction = direction
5
5
  super
6
+ connection.begin_db_transaction if strong_migrations_checker.transaction_disabled
6
7
  end
7
8
 
8
9
  def method_missing(method, *args)
@@ -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
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.7.2"
2
+ VERSION = "0.7.3"
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: 0.7.2
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-10-25 00:00:00.000000000 Z
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