safe-pg-migrations 2.1.0 → 2.2.1

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: 2fafd98448f0ff697da386a337dc22753be2144feedba4793f87a351581a5607
4
- data.tar.gz: 9653ae6395d4c9f27b69dba9dee9142a4535384c42e8d05fac9ca2e2ef1bdce6
3
+ metadata.gz: 624821b7134dee05186350edada6f27faaf8c820976837d32403dc5fffa4880a
4
+ data.tar.gz: bf41324b8d563de7bc893a302d45d8f175e890bb888a2f52e0b14be7676f942c
5
5
  SHA512:
6
- metadata.gz: 64297c9ba4f040e7afa21fefa2acb2a26e27fb0bfee3d9f9cbf902ee4bb0ac087f4e22591a7b5470cf73dc7bc73345e59904649c02d7540bd4693372e7069549
7
- data.tar.gz: 7a2640fd2494480d560cfa6f19059550ecc316eaf729df6d8f97a434c189f4565c49d47acbb02f437d9b1285a466a5615a79a84d6ba1dfb3d94bb54f87b9e788
6
+ metadata.gz: 76601dcf0b97013edc36de6ac351beb8e4f6c532a0e223cba95d1a29e4eb7f18b8a4e7b3e3f28e4e7bde02144a4de8c512c19e2725396578dcb322e040963168
7
+ data.tar.gz: 767876e75af3292f9c4d3fc82810ab37bb3d38dad7b8b87ee6ba7068ec5e4aa9967ac0e53170be77bda07dd3424f2d3b3b3a03d9b8dc6ebcda94cee21e61fcfc
data/README.md CHANGED
@@ -119,7 +119,9 @@ PG will still needs to update every row of the table, and will most likely state
119
119
  **Safe PG Migrations** provides the extra option parameter `default_value_backfill:`. When your migration is adding a volatile default value, the option `:update_in_batches` can be set. It will automatically backfill the value in a safe manner.
120
120
 
121
121
  ```ruby
122
- add_column :users, :created_at, default: 'clock_timestamp()', default_value_backfill: :update_in_batches
122
+ safety_assured do
123
+ add_column :users, :created_at, default: 'clock_timestamp()', default_value_backfill: :update_in_batches
124
+ end
123
125
  ```
124
126
 
125
127
  More specifically, it will:
@@ -142,11 +144,17 @@ Data backfill take time. If your table is big, your migrations will (safely) han
142
144
 
143
145
  2. manual data backfill (rake task, manual operation, ...)
144
146
  3. Second migration which change the column to null false (with **Safe PG Migrations**, `change_column_null` is safe and can be used; see section below)
145
-
146
147
  ---
147
148
 
148
149
  `default_value_backfill:` also accept the value `:auto` which is set by default. In this case, **Safe PG Migrations** will not backfill data and will let PostgreSQL handle it itself.
149
150
 
151
+ ### Preventing :update_in_batches when the table is too big
152
+
153
+ `add_column` with `default_value_backfill: :update_in_batches` can be dangerous on big tables. To avoid unwanted long migrations, **Safe PG Migrations** does not automatically mark this usage as safe when used with `strong-migrations`, usage of `safety_assured` is required.
154
+
155
+ It is also possible to set a threshold for the table size, above which the migration will fail. This can be done by setting the `default_value_backfill_threshold:` option in the configuration.
156
+
157
+
150
158
  </details>
151
159
 
152
160
  <details><summary id="safe_add_remove_index">Safe <code>add_index</code> and <code>remove_index</code></summary>
@@ -238,6 +246,35 @@ When a statement fails with a lock timeout, **Safe PG Migrations** retries it (5
238
246
  <details><summary>Blocking activity logging</summary>
239
247
 
240
248
  If a statement fails with a lock timeout, **Safe PG Migrations** will try to tell you what was the blocking statement.
249
+
250
+ ---
251
+ **NOTE**
252
+
253
+ Data logged by the Blocking activity logger can be sensitive (it will contain raw SQL queries, which can be hashes of password, user information, ...)
254
+
255
+ If you cannot afford to log this type of data, you can either
256
+ * Set `SafePgMigrations.config.blocking_activity_logger_verbose = false`. In this case, the logger will only log the pid of the blocking statement, which should be enough to investigate;
257
+ * Provide a different logger through `SafePgMigrations.config.sensitive_logger = YourLogger.new`. Instead of using the default IO stream, SafePgMigrations will send sensitive data to the given logger which can be managed as you wish.
258
+
259
+ ---
260
+
261
+ </details>
262
+
263
+ <details><summary>Dropping a table</summary>
264
+
265
+ Dropping a table can be difficult to achieve in a small amount of time if it holds several foreign keys to busy tables.
266
+ To remove the table, PostgreSQL will have to acquire an access exclusive lock on all the tables referenced by the foreign keys.
267
+
268
+ To solve this issue, **Safe Pg Migrations** will drop the foreign keys before dropping the table.
269
+
270
+ ---
271
+ **NOTE**
272
+
273
+ Dropping a table is a dangerous operation by nature. **Safe Pg Migrations** will not prevent the deletion of a table which
274
+ would still be in use.
275
+
276
+ ---
277
+
241
278
  </details>
242
279
 
243
280
  <details><summary>Verbose SQL logging</summary>
@@ -288,33 +325,32 @@ So you can actually check that the `CREATE INDEX` statement will be performed co
288
325
  **Safe PG Migrations** can be customized, here is an example of a Rails initializer (the values are the default ones):
289
326
 
290
327
  ```ruby
291
- SafePgMigrations.config.safe_timeout = 5.seconds # Lock and statement timeout used for all DDL operations except from CREATE / DROP INDEX
328
+ SafePgMigrations.config.safe_timeout = 5.seconds # Statement timeout used for all DDL operations except from CREATE / DROP INDEX
329
+
330
+ SafePgMigrations.config.lock_timeout = nil # Lock timeout used for all DDL operations except from CREATE / DROP INDEX. If not set, safe_timeout will be used with a deduction of 1% to ensure that the lock timeout is raised in priority
292
331
 
293
332
  SafePgMigrations.config.blocking_activity_logger_verbose = true # Outputs the raw blocking queries on timeout. When false, outputs information about the lock instead
294
333
 
334
+ SafePgMigrations.config.sensitive_logger = nil # When given, sensitive data will be sent to this logger instead of the standard output. Must implement method `info`.
335
+
295
336
  SafePgMigrations.config.blocking_activity_logger_margin = 1.second # Delay to output blocking queries before timeout. Must be shorter than safe_timeout
296
337
 
297
338
  SafePgMigrations.config.backfill_batch_size = 100_000 # Size of the batches used for backfilling when adding a column with a default value
298
339
 
299
340
  SafePgMigrations.config.backfill_pause = 0.5.second # Delay between each batch during a backfill. This ensure replication can happen safely.
300
341
 
342
+ SafePgMigrations.config.default_value_backfill_threshold = nil # When set, batch backfill will only be available if the table is under the given threshold. If the number of rows is higher (according to stats), the migration will fail.
343
+
301
344
  SafePgMigrations.config.retry_delay = 1.minute # Delay between retries for retryable statements
302
345
 
303
346
  SafePgMigrations.config.max_tries = 5 # Number of retries before abortion of the migration
304
347
  ```
305
348
 
306
- ## Running tests
307
-
308
- ```bash
309
- bundle
310
- psql -h localhost -c 'CREATE DATABASE safe_pg_migrations_test'
311
- rake test
312
- ```
313
-
314
349
  ## Authors
315
350
 
316
351
  - [Matthieu Prat](https://github.com/matthieuprat)
317
352
  - [Romain Choquet](https://github.com/rchoquet)
353
+ - [Thomas Hareau](https://github.com/ThHareau)
318
354
  - [Paul-Etienne Coisne](https://github.com/coisnepe)
319
355
 
320
356
  ## License
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'safe-pg-migrations/configuration'
4
+ require 'safe-pg-migrations/helpers/logger'
4
5
  require 'safe-pg-migrations/helpers/satisfied_helper'
5
6
  require 'safe-pg-migrations/helpers/index_helper'
6
7
  require 'safe-pg-migrations/helpers/batch_over'
8
+ require 'safe-pg-migrations/helpers/session_setting_management'
7
9
  require 'safe-pg-migrations/plugins/verbose_sql_logger'
8
10
  require 'safe-pg-migrations/plugins/blocking_activity_logger'
9
11
  require 'safe-pg-migrations/plugins/statement_insurer/add_column'
12
+ require 'safe-pg-migrations/plugins/statement_insurer/change_column_null'
10
13
  require 'safe-pg-migrations/plugins/statement_insurer'
11
14
  require 'safe-pg-migrations/plugins/statement_retrier'
12
15
  require 'safe-pg-migrations/plugins/idempotent_statements'
13
16
  require 'safe-pg-migrations/plugins/useless_statements_logger'
17
+ require 'safe-pg-migrations/plugins/strong_migrations_integration'
14
18
  require 'safe-pg-migrations/polyfills/index_definition_polyfill'
15
19
  require 'safe-pg-migrations/polyfills/verbose_query_logs_polyfill'
16
20
 
@@ -31,15 +35,29 @@ module SafePgMigrations
31
35
  def setup_and_teardown(migration, connection, &block)
32
36
  @pg_version_num = get_pg_version_num(connection)
33
37
  @alternate_connection = nil
34
- @current_migration = migration
35
- stdout_sql_logger = VerboseSqlLogger.new.setup if verbose?
36
- PLUGINS.each { |plugin| connection.extend(plugin) }
37
38
 
38
- connection.with_setting :lock_timeout, SafePgMigrations.config.pg_lock_timeout, &block
39
+ with_current_migration(migration) do
40
+ stdout_sql_logger = VerboseSqlLogger.new.setup if verbose?
41
+
42
+ VerboseSqlLogger.new.setup if verbose?
43
+ PLUGINS.each { |plugin| connection.extend(plugin) }
44
+
45
+ connection.with_setting :lock_timeout, SafePgMigrations.config.pg_lock_timeout do
46
+ connection.with_setting :statement_timeout, SafePgMigrations.config.pg_statement_timeout, &block
47
+ end
48
+ ensure
49
+ stdout_sql_logger&.teardown
50
+ end
39
51
  ensure
40
52
  close_alternate_connection
53
+ end
54
+
55
+ def with_current_migration(migration, &block)
56
+ @current_migration = migration
57
+
58
+ yield block
59
+ ensure
41
60
  @current_migration = nil
42
- stdout_sql_logger&.teardown
43
61
  end
44
62
 
45
63
  def alternate_connection
@@ -53,16 +71,6 @@ module SafePgMigrations
53
71
  @alternate_connection = nil
54
72
  end
55
73
 
56
- ruby2_keywords def say(*args)
57
- return unless current_migration
58
-
59
- current_migration.say(*args)
60
- end
61
-
62
- ruby2_keywords def say_method_call(method, *args)
63
- say "#{method}(#{args.map(&:inspect) * ', '})", true
64
- end
65
-
66
74
  def verbose?
67
75
  unless current_migration.class._safe_pg_migrations_verbose.nil?
68
76
  return current_migration.class._safe_pg_migrations_verbose
@@ -83,6 +91,8 @@ module SafePgMigrations
83
91
  end
84
92
 
85
93
  module Migration
94
+ include StrongMigrationsIntegration
95
+
86
96
  module ClassMethods
87
97
  attr_accessor :_safe_pg_migrations_verbose
88
98
 
@@ -98,28 +108,11 @@ module SafePgMigrations
98
108
  end
99
109
 
100
110
  def disable_ddl_transaction
101
- UselessStatementsLogger.warn_useless '`disable_ddl_transaction`' if super
102
- true
103
- end
111
+ SafePgMigrations.with_current_migration(self) do
112
+ UselessStatementsLogger.warn_useless '`disable_ddl_transaction`' if super
104
113
 
105
- SAFE_METHODS = %i[
106
- execute
107
- add_column
108
- add_index
109
- add_reference
110
- add_belongs_to
111
- change_column_null
112
- add_foreign_key
113
- add_check_constraint
114
- ].freeze
115
-
116
- SAFE_METHODS.each do |method|
117
- define_method method do |*args|
118
- return super(*args) unless respond_to?(:safety_assured)
119
-
120
- safety_assured { super(*args) }
114
+ true
121
115
  end
122
- ruby2_keywords method
123
116
  end
124
117
  end
125
118
  end
@@ -4,17 +4,49 @@ require 'active_support/core_ext/numeric/time'
4
4
 
5
5
  module SafePgMigrations
6
6
  class Configuration
7
- attr_accessor :safe_timeout, :blocking_activity_logger_margin, :blocking_activity_logger_verbose,
8
- :backfill_batch_size, :backfill_pause, :retry_delay, :max_tries
7
+ attr_accessor(*%i[
8
+ blocking_activity_logger_margin
9
+ blocking_activity_logger_verbose
10
+ default_value_backfill_threshold
11
+ backfill_batch_size
12
+ backfill_pause
13
+ retry_delay
14
+ max_tries
15
+ sensitive_logger
16
+ ])
17
+ attr_reader :lock_timeout, :safe_timeout
9
18
 
10
19
  def initialize
20
+ self.default_value_backfill_threshold = nil
11
21
  self.safe_timeout = 5.seconds
22
+ self.lock_timeout = nil
12
23
  self.blocking_activity_logger_margin = 1.second
13
24
  self.blocking_activity_logger_verbose = true
14
25
  self.backfill_batch_size = 100_000
15
26
  self.backfill_pause = 0.5.second
16
27
  self.retry_delay = 1.minute
17
28
  self.max_tries = 5
29
+ self.sensitive_logger = nil
30
+ end
31
+
32
+ def lock_timeout=(value)
33
+ raise 'Setting lock timeout to 0 disables the lock timeout and is dangerous' if value == 0.seconds
34
+
35
+ unless value.nil? || value < safe_timeout
36
+ raise ArgumentError, "Lock timeout (#{value}) cannot be greater than safe timeout (#{safe_timeout})"
37
+ end
38
+
39
+ @lock_timeout = value
40
+ end
41
+
42
+ def safe_timeout=(value)
43
+ raise 'Setting safe timeout to 0 disables the safe timeout and is dangerous' unless value
44
+
45
+ unless lock_timeout.nil? || value > lock_timeout
46
+ raise ArgumentError, "Safe timeout (#{value}) cannot be less than lock timeout (#{lock_timeout})"
47
+ end
48
+
49
+ @safe_timeout = value
18
50
  end
19
51
 
20
52
  def pg_statement_timeout
@@ -22,6 +54,8 @@ module SafePgMigrations
22
54
  end
23
55
 
24
56
  def pg_lock_timeout
57
+ return pg_duration lock_timeout if lock_timeout
58
+
25
59
  # if statement timeout and lock timeout have the same value, statement timeout will raise in priority. We actually
26
60
  # need the opposite for BlockingActivityLogger to detect lock timeouts correctly.
27
61
  # By reducing the lock timeout by a very small margin, we ensure that the lock timeout is raised in priority
@@ -5,20 +5,20 @@ module SafePgMigrations
5
5
  module BlockingActivityFormatter
6
6
  def log_queries(queries)
7
7
  if queries.empty?
8
- SafePgMigrations.say 'Could not find any blocking query.', true
8
+ Logger.say 'Could not find any blocking query.', sub_item: true
9
9
  else
10
- SafePgMigrations.say(
11
- "Statement was being blocked by the following #{'query'.pluralize(queries.size)}:",
12
- true
13
- )
14
- SafePgMigrations.say '', true
10
+ Logger.say <<~MESSAGE.squish, sub_item: true
11
+ Statement was being blocked by the following #{'query'.pluralize(queries.size)}:
12
+ MESSAGE
13
+
14
+ Logger.say '', sub_item: true
15
15
  output_blocking_queries(queries)
16
- SafePgMigrations.say(
17
- 'Beware, some of those queries might run in a transaction. In this case the locking query might be ' \
18
- 'located elsewhere in the transaction',
19
- true
20
- )
21
- SafePgMigrations.say '', true
16
+ Logger.say <<~MESSAGE.squish, sub_item: true
17
+ Beware, some of those queries might run in a transaction. In this case the locking query might be located
18
+ elsewhere in the transaction
19
+ MESSAGE
20
+
21
+ Logger.say '', sub_item: true
22
22
  end
23
23
  end
24
24
 
@@ -27,8 +27,10 @@ module SafePgMigrations
27
27
  def output_blocking_queries(queries)
28
28
  if SafePgMigrations.config.blocking_activity_logger_verbose
29
29
  queries.each do |pid, query, start_time|
30
- SafePgMigrations.say "Query with pid #{pid || 'null'} started #{format_start_time start_time}: #{query}",
31
- true
30
+ Logger.say(
31
+ "Query with pid #{pid || 'null'} started #{format_start_time start_time}: #{query}",
32
+ sub_item: true, sensitive: true
33
+ )
32
34
  end
33
35
  else
34
36
  output_confidentially_blocking_queries(queries)
@@ -37,14 +39,13 @@ module SafePgMigrations
37
39
 
38
40
  def output_confidentially_blocking_queries(queries)
39
41
  queries.each do |start_time, locktype, mode, pid, transactionid|
40
- SafePgMigrations.say(
41
- "Query with pid #{pid || 'null'} " \
42
- "started #{format_start_time(start_time)}: " \
43
- "lock type: #{locktype || 'null'}, " \
44
- "lock mode: #{mode || 'null'}, " \
45
- "lock transactionid: #{transactionid || 'null'}",
46
- true
47
- )
42
+ Logger.say <<~MESSAGE.squish, sub_item: true, sensitive: true
43
+ Query with pid #{pid || 'null'}
44
+ started #{format_start_time(start_time)}:
45
+ lock type: #{locktype || 'null'},
46
+ lock mode: #{mode || 'null'},
47
+ lock transactionid: #{transactionid || 'null'}",
48
+ MESSAGE
48
49
  end
49
50
  end
50
51
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module Helpers
5
+ module Logger
6
+ class << self
7
+ def say(message, sub_item: false, sensitive: false, warn_sensitive_logs: true)
8
+ return unless SafePgMigrations.current_migration
9
+
10
+ if sensitive
11
+ log_sensitive message, sub_item: sub_item
12
+ if warn_sensitive_logs && sensitive_logger?
13
+ log 'Sensitive data sent to sensitive logger', sub_item: sub_item
14
+ end
15
+ else
16
+ log message, sub_item: sub_item
17
+ end
18
+ end
19
+
20
+ def say_method_call(method, *args, sensitive: false, warn_sensitive_logs: true, **options)
21
+ args += [options] unless options.empty?
22
+
23
+ say "#{method}(#{args.map(&:inspect) * ', '})",
24
+ sub_item: true, sensitive: sensitive, warn_sensitive_logs: warn_sensitive_logs
25
+ end
26
+
27
+ private
28
+
29
+ def log(message, sub_item:)
30
+ SafePgMigrations.current_migration.say message, sub_item
31
+ end
32
+
33
+ def log_sensitive(message, sub_item:)
34
+ if sensitive_logger?
35
+ SafePgMigrations.config.sensitive_logger.info message
36
+ else
37
+ log message, sub_item: sub_item
38
+ end
39
+ end
40
+
41
+ def sensitive_logger?
42
+ SafePgMigrations.config.sensitive_logger
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module Helpers
5
+ module SessionSettingManagement
6
+ def with_setting(key, value)
7
+ old_value = query_value("SHOW #{key}")
8
+ execute("SET #{key} TO #{quote(value)}")
9
+ begin
10
+ yield
11
+ ensure
12
+ begin
13
+ execute("SET #{key} TO #{quote(old_value)}")
14
+ rescue ActiveRecord::StatementInvalid => e
15
+ # Swallow `PG::InFailedSqlTransaction` exceptions so as to keep the
16
+ # original exception (if any).
17
+ raise unless e.cause.is_a?(PG::InFailedSqlTransaction)
18
+ end
19
+ end
20
+ end
21
+
22
+ def without_statement_timeout(&block)
23
+ with_setting(:statement_timeout, 0, &block)
24
+ end
25
+
26
+ def without_lock_timeout(&block)
27
+ with_setting(:lock_timeout, 0, &block)
28
+ end
29
+
30
+ def without_timeout(&block)
31
+ without_statement_timeout { without_lock_timeout(&block) }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -5,8 +5,8 @@ require_relative '../helpers/blocking_activity_selector'
5
5
 
6
6
  module SafePgMigrations
7
7
  module BlockingActivityLogger
8
- include ::SafePgMigrations::Helpers::BlockingActivityFormatter
9
- include ::SafePgMigrations::Helpers::BlockingActivitySelector
8
+ include Helpers::BlockingActivityFormatter
9
+ include Helpers::BlockingActivitySelector
10
10
 
11
11
  %i[
12
12
  add_column
@@ -18,7 +18,19 @@ module SafePgMigrations
18
18
  create_table
19
19
  ].each do |method|
20
20
  define_method method do |*args, &block|
21
- log_blocking_queries_after_lock { super(*args, &block) }
21
+ log_context = lambda do
22
+ break unless SafePgMigrations.config.sensitive_logger
23
+
24
+ options = args.last.is_a?(Hash) ? args.last : {}
25
+
26
+ Helpers::Logger.say "Executing #{SafePgMigrations.current_migration.name}",
27
+ sensitive: true, warn_sensitive_logs: false
28
+ Helpers::Logger.say_method_call method, *args, **options, sensitive: true, warn_sensitive_logs: false
29
+ end
30
+
31
+ log_blocking_queries_after_lock(log_context) do
32
+ super(*args, &block)
33
+ end
22
34
  end
23
35
  ruby2_keywords method
24
36
  end
@@ -50,7 +62,7 @@ module SafePgMigrations
50
62
  blocking_queries_retriever_thread.kill
51
63
  end
52
64
 
53
- def log_blocking_queries_after_lock
65
+ def log_blocking_queries_after_lock(log_context)
54
66
  blocking_queries_retriever_thread =
55
67
  Thread.new do
56
68
  sleep delay_before_logging
@@ -63,14 +75,15 @@ module SafePgMigrations
63
75
 
64
76
  blocking_queries_retriever_thread.kill
65
77
  rescue ActiveRecord::LockWaitTimeout
66
- SafePgMigrations.say 'Lock timeout.', true
78
+ Helpers::Logger.say 'Lock timeout.', sub_item: true
79
+ log_context.call
67
80
  queries =
68
81
  begin
69
82
  blocking_queries_retriever_thread.value
70
83
  rescue StandardError => e
71
- SafePgMigrations.say(
84
+ Helpers::Logger.say(
72
85
  "Error while retrieving blocking queries: #{e}",
73
- true
86
+ sub_item: true
74
87
  )
75
88
  nil
76
89
  end
@@ -81,12 +94,15 @@ module SafePgMigrations
81
94
  end
82
95
 
83
96
  def delay_before_logging
84
- SafePgMigrations.config.safe_timeout -
85
- SafePgMigrations.config.blocking_activity_logger_margin
97
+ timeout - SafePgMigrations.config.blocking_activity_logger_margin
86
98
  end
87
99
 
88
100
  def delay_before_retry
89
101
  SafePgMigrations.config.blocking_activity_logger_margin + SafePgMigrations.config.retry_delay
90
102
  end
103
+
104
+ def timeout
105
+ SafePgMigrations.config.lock_timeout || SafePgMigrations.config.safe_timeout
106
+ end
91
107
  end
92
108
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module SafePgMigrations
4
4
  module IdempotentStatements
5
- include SafePgMigrations::Helpers::IndexHelper
5
+ include Helpers::IndexHelper
6
6
 
7
7
  ruby2_keywords def add_index(table_name, column_name, *args)
8
8
  options = args.last.is_a?(Hash) ? args.last : {}
@@ -12,10 +12,9 @@ module SafePgMigrations
12
12
  return super unless index_name_exists?(index_definition.table, index_definition.name)
13
13
 
14
14
  if index_valid?(index_definition.name)
15
- SafePgMigrations.say(
16
- "/!\\ Index '#{index_definition.name}' already exists in '#{table_name}'. Skipping statement.",
17
- true
18
- )
15
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
16
+ /!\\ Index '#{index_definition.name}' already exists in '#{table_name}'. Skipping statement.
17
+ MESSAGE
19
18
  return
20
19
  end
21
20
 
@@ -26,13 +25,17 @@ module SafePgMigrations
26
25
  ruby2_keywords def add_column(table_name, column_name, type, *)
27
26
  return super unless column_exists?(table_name, column_name)
28
27
 
29
- SafePgMigrations.say("/!\\ Column '#{column_name}' already exists in '#{table_name}'. Skipping statement.", true)
28
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
29
+ /!\\ Column '#{column_name}' already exists in '#{table_name}'. Skipping statement.
30
+ MESSAGE
30
31
  end
31
32
 
32
33
  ruby2_keywords def remove_column(table_name, column_name, type = nil, *)
33
34
  return super if column_exists?(table_name, column_name)
34
35
 
35
- SafePgMigrations.say("/!\\ Column '#{column_name}' not found on table '#{table_name}'. Skipping statement.", true)
36
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
37
+ /!\\ Column '#{column_name}' not found on table '#{table_name}'. Skipping statement.
38
+ MESSAGE
36
39
  end
37
40
 
38
41
  ruby2_keywords def remove_index(table_name, *args)
@@ -41,41 +44,41 @@ module SafePgMigrations
41
44
 
42
45
  return super if index_name_exists?(table_name, index_name)
43
46
 
44
- SafePgMigrations.say("/!\\ Index '#{index_name}' not found on table '#{table_name}'. Skipping statement.", true)
47
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
48
+ /!\\ Index '#{index_name}' not found on table '#{table_name}'. Skipping statement.
49
+ MESSAGE
45
50
  end
46
51
 
47
52
  ruby2_keywords def add_foreign_key(from_table, to_table, *args)
48
53
  options = args.last.is_a?(Hash) ? args.last : {}
49
- suboptions = options.slice(:name, :column)
50
- return super unless foreign_key_exists?(from_table, suboptions.present? ? nil : to_table, **suboptions)
54
+ sub_options = options.slice(:name, :column)
55
+ return super unless foreign_key_exists?(from_table, sub_options.present? ? nil : to_table, **sub_options)
51
56
 
52
- SafePgMigrations.say(
53
- "/!\\ Foreign key '#{from_table}' -> '#{to_table}' already exists. Skipping statement.",
54
- true
55
- )
57
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
58
+ /!\\ Foreign key '#{from_table}' -> '#{to_table}' already exists. Skipping statement.
59
+ MESSAGE
56
60
  end
57
61
 
58
62
  def remove_foreign_key(from_table, to_table = nil, **options)
59
63
  return super if foreign_key_exists?(from_table, to_table, **options)
60
64
 
61
65
  reference_name = to_table || options[:to_table] || options[:column] || options[:name]
62
- SafePgMigrations.say(
63
- "/!\\ Foreign key '#{from_table}' -> '#{reference_name}' does not exist. Skipping statement.",
64
- true
65
- )
66
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
67
+ /!\\ Foreign key '#{from_table}' -> '#{reference_name}' does not exist. Skipping statement.
68
+ MESSAGE
66
69
  end
67
70
 
68
71
  ruby2_keywords def create_table(table_name, *args)
69
72
  options = args.last.is_a?(Hash) ? args.last : {}
70
73
  return super if options[:force] || !table_exists?(table_name)
71
74
 
72
- SafePgMigrations.say "/!\\ Table '#{table_name}' already exists.", true
75
+ Helpers::Logger.say "/!\\ Table '#{table_name}' already exists.", sub_item: true
73
76
 
74
77
  td = create_table_definition(table_name, *args)
75
78
 
76
79
  yield td if block_given?
77
80
 
78
- SafePgMigrations.say(td.indexes.empty? ? '-- Skipping statement' : '-- Creating indexes', true)
81
+ Helpers::Logger.say td.indexes.empty? ? '-- Skipping statement' : '-- Creating indexes', sub_item: true
79
82
 
80
83
  td.indexes.each do |column_name, index_options|
81
84
  add_index(table_name, column_name, **index_options)
@@ -88,7 +91,9 @@ module SafePgMigrations
88
91
 
89
92
  return super if constraint_definition.nil?
90
93
 
91
- SafePgMigrations.say "/!\\ Constraint '#{constraint_definition.name}' already exists. Skipping statement.", true
94
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
95
+ /!\\ Constraint '#{constraint_definition.name}' already exists. Skipping statement.
96
+ MESSAGE
92
97
  end
93
98
 
94
99
  def change_column_null(table_name, column_name, null, *)
@@ -96,10 +101,9 @@ module SafePgMigrations
96
101
 
97
102
  return super if column.null != null
98
103
 
99
- SafePgMigrations.say(
100
- "/!\\ Column '#{table_name}.#{column.name}' is already set to 'null: #{null}'. Skipping statement.",
101
- true
102
- )
104
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
105
+ /!\\ Column '#{table_name}.#{column.name}' is already set to 'null: #{null}'. Skipping statement.
106
+ MESSAGE
103
107
  end
104
108
 
105
109
  def validate_check_constraint(table_name, **options)
@@ -107,8 +111,9 @@ module SafePgMigrations
107
111
 
108
112
  return super unless constraint_definition.validated?
109
113
 
110
- SafePgMigrations.say "/!\\ Constraint '#{constraint_definition.name}' already validated. Skipping statement.",
111
- true
114
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
115
+ /!\\ Constraint '#{constraint_definition.name}' already validated. Skipping statement.
116
+ MESSAGE
112
117
  end
113
118
 
114
119
  def change_column_default(table_name, column_name, default_or_changes)
@@ -122,11 +127,17 @@ module SafePgMigrations
122
127
 
123
128
  return super if new_alter_statement != previous_alter_statement
124
129
 
125
- SafePgMigrations.say(
126
- "/!\\ Column '#{table_name}.#{column.name}' is already set to 'default: #{column.default}'. " \
127
- 'Skipping statement.',
128
- true
129
- )
130
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
131
+ /!\\ Column '#{table_name}.#{column.name}' is already set to 'default: #{column.default}'. Skipping statement.
132
+ MESSAGE
133
+ end
134
+
135
+ ruby2_keywords def drop_table(table_name, *)
136
+ return super if table_exists?(table_name)
137
+
138
+ Helpers::Logger.say <<~MESSAGE.squish, sub_item: true
139
+ /!\\ Table '#{table_name} does not exist. Skipping statement.
140
+ MESSAGE
130
141
  end
131
142
  end
132
143
  end
@@ -7,25 +7,27 @@ module SafePgMigrations
7
7
  options = args.last.is_a?(Hash) && args.last
8
8
  options ||= {}
9
9
 
10
- if should_keep_default_implementation?(**options)
11
- with_setting(:statement_timeout, SafePgMigrations.config.pg_statement_timeout) { return super }
12
- end
10
+ return super if should_keep_default_implementation?(**options)
11
+
12
+ raise <<~ERROR unless backfill_column_default_safe?(table_name)
13
+ Table #{table_name} has more than #{SafePgMigrations.config.default_value_backfill_threshold} rows.
14
+ Backfilling the default value for column #{column_name} on table #{table_name} would take too long.
15
+
16
+ Please revert this migration, and backfill the default value manually.
17
+
18
+ This check is configurable through the configuration "default_value_backfill_threshold".
19
+ ERROR
13
20
 
14
21
  default = options.delete(:default)
15
22
  null = options.delete(:null)
16
23
 
17
- with_setting(:statement_timeout, SafePgMigrations.config.pg_statement_timeout) do
18
- SafePgMigrations.say_method_call(:add_column, table_name, column_name, type, options)
19
- super table_name, column_name, type, **options
20
- end
24
+ Helpers::Logger.say_method_call(:add_column, table_name, column_name, type, options)
25
+ super table_name, column_name, type, **options
21
26
 
22
- SafePgMigrations.say_method_call(:change_column_default, table_name, column_name, default)
27
+ Helpers::Logger.say_method_call(:change_column_default, table_name, column_name, default)
23
28
  change_column_default(table_name, column_name, default)
24
29
 
25
- SafePgMigrations.say_method_call(:backfill_column_default, table_name, column_name)
26
- without_statement_timeout do
27
- backfill_column_default(table_name, column_name)
28
- end
30
+ backfill_column_default(table_name, column_name)
29
31
 
30
32
  change_column_null(table_name, column_name, null) if null == false
31
33
  end
@@ -34,16 +36,27 @@ module SafePgMigrations
34
36
 
35
37
  def should_keep_default_implementation?(default: nil, default_value_backfill: :auto, **)
36
38
  default_value_backfill != :update_in_batches || !default ||
37
- !SafePgMigrations::Helpers::SatisfiedHelper.satisfies_add_column_update_rows_backfill?
39
+ !Helpers::SatisfiedHelper.satisfies_add_column_update_rows_backfill?
40
+ end
41
+
42
+ def backfill_column_default_safe?(table_name)
43
+ return true if SafePgMigrations.config.default_value_backfill_threshold.nil?
44
+
45
+ row, = query("SELECT reltuples AS estimate FROM pg_class where relname = '#{table_name}';")
46
+ estimate, = row
47
+
48
+ estimate <= SafePgMigrations.config.default_value_backfill_threshold
38
49
  end
39
50
 
40
51
  def backfill_column_default(table_name, column_name)
41
52
  model = Class.new(ActiveRecord::Base) { self.table_name = table_name }
42
53
  quoted_column_name = quote_column_name(column_name)
43
54
 
44
- SafePgMigrations::Helpers::BatchOver.new(model).each_batch do |batch|
45
- batch
46
- .update_all("#{quoted_column_name} = DEFAULT")
55
+ Helpers::Logger.say_method_call(:backfill_column_default, table_name, column_name)
56
+
57
+ Helpers::BatchOver.new(model).each_batch do |batch|
58
+ batch.update_all("#{quoted_column_name} = DEFAULT")
59
+
47
60
  sleep SafePgMigrations.config.backfill_pause
48
61
  end
49
62
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module StatementInsurer
5
+ module ChangeColumnNull
6
+ def change_column_null(table_name, column_name, null, default = nil)
7
+ return super unless should_create_constraint? default, null
8
+
9
+ expression = "#{column_name} IS NOT NULL"
10
+ # constraint will be defined if the constraint was manually created in another migration
11
+ constraint = check_constraint_by_expression table_name, expression
12
+
13
+ default_name = check_constraint_name(table_name, expression: expression)
14
+ constraint_name = constraint&.name || default_name
15
+
16
+ add_check_constraint table_name, expression, name: constraint_name
17
+
18
+ Helpers::Logger.say_method_call :change_column_null, table_name, column_name, false
19
+ super table_name, column_name, false
20
+
21
+ return unless should_remove_constraint? default_name, constraint_name
22
+
23
+ Helpers::Logger.say_method_call :remove_check_constraint, table_name, expression, name: constraint_name
24
+ remove_check_constraint table_name, expression, name: constraint_name
25
+ end
26
+
27
+ private
28
+
29
+ def check_constraint_by_expression(table_name, expression)
30
+ check_constraints(table_name).detect { |check_constraint| check_constraint.expression = expression }
31
+ end
32
+
33
+ def should_create_constraint?(default, null)
34
+ !default && !null && Helpers::SatisfiedHelper.satisfies_change_column_null_requirements?
35
+ end
36
+
37
+ def should_remove_constraint?(default_name, constraint_name)
38
+ # we don't want to remove the constraint if it was created in another migration. The best guess we have here is
39
+ # that manually created constraint would likely have a name that is not the default name. This is not a perfect,
40
+ # a manually created constraint without a name would be removed. However, it is now replaced by the NOT NULL
41
+ # statement on the table, so this is not a big issue.
42
+ default_name == constraint_name
43
+ end
44
+ end
45
+ end
46
+ end
@@ -2,19 +2,13 @@
2
2
 
3
3
  module SafePgMigrations
4
4
  module StatementInsurer
5
+ include Helpers::SessionSettingManagement
5
6
  include AddColumn
6
-
7
- %i[change_column].each do |method|
8
- define_method method do |*args, &block|
9
- with_setting(:statement_timeout, SafePgMigrations.config.pg_statement_timeout) { super(*args, &block) }
10
- end
11
- ruby2_keywords method
12
- end
7
+ include ChangeColumnNull
13
8
 
14
9
  def validate_check_constraint(table_name, **options)
15
- without_statement_timeout do
16
- super
17
- end
10
+ Helpers::Logger.say_method_call :validate_check_constraint, table_name, **options
11
+ without_statement_timeout { super }
18
12
  end
19
13
 
20
14
  def add_check_constraint(table_name, expression, **options)
@@ -23,37 +17,38 @@ module SafePgMigrations
23
17
 
24
18
  options = check_constraint_options(table_name, expression, options)
25
19
 
26
- SafePgMigrations.say_method_call :add_check_constraint, table_name, expression, **options, validate: false
20
+ Helpers::Logger.say_method_call :add_check_constraint, table_name, expression, **options,
21
+ validate: false
27
22
  super table_name, expression, **options, validate: false
28
23
 
29
24
  return unless options.fetch(:validate, true)
30
25
 
31
- SafePgMigrations.say_method_call :validate_check_constraint, table_name, name: options[:name]
32
26
  validate_check_constraint table_name, name: options[:name]
33
27
  end
34
28
 
29
+ def validate_foreign_key(*, **)
30
+ without_statement_timeout { super }
31
+ end
32
+
35
33
  ruby2_keywords def add_foreign_key(from_table, to_table, *args)
36
34
  options = args.last.is_a?(Hash) ? args.last : {}
37
35
  validate_present = options.key?(:validate)
38
36
  options[:validate] = false unless validate_present
39
- with_setting(:statement_timeout, SafePgMigrations.config.pg_statement_timeout) do
40
- super(from_table, to_table, **options)
41
- end
37
+
38
+ super(from_table, to_table, **options)
42
39
 
43
40
  return if validate_present
44
41
 
45
- suboptions = options.slice(:name, :column)
46
- without_statement_timeout { validate_foreign_key from_table, suboptions.present? ? nil : to_table, **suboptions }
42
+ sub_options = options.slice(:name, :column)
43
+ validate_foreign_key from_table, sub_options.present? ? nil : to_table, **sub_options
47
44
  end
48
45
 
49
46
  ruby2_keywords def create_table(*)
50
- with_setting(:statement_timeout, SafePgMigrations.config.pg_statement_timeout) do
51
- super do |td|
52
- yield td if block_given?
53
- td.indexes.map! do |key, index_options|
54
- index_options[:algorithm] ||= :default
55
- [key, index_options]
56
- end
47
+ super do |td|
48
+ yield td if block_given?
49
+ td.indexes.map! do |key, index_options|
50
+ index_options[:algorithm] ||= :default
51
+ [key, index_options]
57
52
  end
58
53
  end
59
54
  end
@@ -67,61 +62,33 @@ module SafePgMigrations
67
62
  options[:algorithm] = :concurrently
68
63
  end
69
64
 
70
- SafePgMigrations.say_method_call(:add_index, table_name, column_name, **options)
71
-
65
+ Helpers::Logger.say_method_call(:add_index, table_name, column_name, **options)
72
66
  without_timeout { super(table_name, column_name, **options) }
73
67
  end
74
68
 
75
69
  ruby2_keywords def remove_index(table_name, *args)
76
70
  options = args.last.is_a?(Hash) ? args.last : { column: args.last }
77
71
  options[:algorithm] = :concurrently unless options.key?(:algorithm)
78
- SafePgMigrations.say_method_call(:remove_index, table_name, **options)
79
72
 
73
+ Helpers::Logger.say_method_call(:remove_index, table_name, **options)
80
74
  without_timeout { super(table_name, **options) }
81
75
  end
82
76
 
83
- def change_column_null(table_name, column_name, null, default = nil)
84
- if default || null || !Helpers::SatisfiedHelper.satisfies_change_column_null_requirements?
85
- with_setting(:statement_timeout, SafePgMigrations.config.pg_statement_timeout) { return super }
86
- end
87
-
88
- add_check_constraint table_name, "#{column_name} IS NOT NULL"
77
+ def remove_column(table_name, column_name, *)
78
+ foreign_key = foreign_key_for(table_name, column: column_name)
89
79
 
90
- SafePgMigrations.say_method_call :change_column_null, table_name, column_name, false
91
- with_setting(:statement_timeout, SafePgMigrations.config.pg_statement_timeout) do
92
- super table_name, column_name, false
93
- end
94
-
95
- SafePgMigrations.say_method_call :remove_check_constraint, table_name, "#{column_name} IS NOT NULL"
96
- remove_check_constraint table_name, "#{column_name} IS NOT NULL"
80
+ remove_foreign_key(table_name, name: foreign_key.name) if foreign_key
81
+ super
97
82
  end
98
83
 
99
- def with_setting(key, value)
100
- old_value = query_value("SHOW #{key}")
101
- execute("SET #{key} TO #{quote(value)}")
102
- begin
103
- yield
104
- ensure
105
- begin
106
- execute("SET #{key} TO #{quote(old_value)}")
107
- rescue ActiveRecord::StatementInvalid => e
108
- # Swallow `PG::InFailedSqlTransaction` exceptions so as to keep the
109
- # original exception (if any).
110
- raise unless e.cause.is_a?(PG::InFailedSqlTransaction)
111
- end
84
+ ruby2_keywords def drop_table(table_name, *args)
85
+ foreign_keys(table_name).each do |foreign_key|
86
+ remove_foreign_key(table_name, name: foreign_key.name)
112
87
  end
113
- end
114
-
115
- def without_statement_timeout(&block)
116
- with_setting(:statement_timeout, 0, &block)
117
- end
118
88
 
119
- def without_lock_timeout(&block)
120
- with_setting(:lock_timeout, 0, &block)
121
- end
89
+ Helpers::Logger.say_method_call :drop_table, table_name, *args
122
90
 
123
- def without_timeout(&block)
124
- without_statement_timeout { without_lock_timeout(&block) }
91
+ super(table_name, *args)
125
92
  end
126
93
  end
127
94
  end
@@ -25,9 +25,9 @@ module SafePgMigrations
25
25
  raise unless remaining_tries > 0
26
26
 
27
27
  retry_delay = SafePgMigrations.config.retry_delay
28
- SafePgMigrations.say "Retrying in #{retry_delay} seconds...", true
28
+ Helpers::Logger.say "Retrying in #{retry_delay} seconds...", sub_item: true
29
29
  sleep retry_delay
30
- SafePgMigrations.say 'Retrying now.', true
30
+ Helpers::Logger.say 'Retrying now.', sub_item: true
31
31
  retry
32
32
  end
33
33
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module StrongMigrationsIntegration
5
+ class << self
6
+ def initialize
7
+ return unless strong_migration_available?
8
+
9
+ StrongMigrations.disable_check(:add_column_default)
10
+ StrongMigrations.disable_check(:add_column_default_callable)
11
+ StrongMigrations.add_check do |method, args|
12
+ next unless method == :add_column
13
+
14
+ options = args.last.is_a?(Hash) ? args.last : {}
15
+
16
+ default_value_backfill = options.fetch(:default_value_backfill, :auto)
17
+
18
+ if default_value_backfill == :update_in_batches
19
+ check_message = <<~CHECK
20
+ default_value_backfill: :update_in_batches will take time if the table is too big.
21
+
22
+ Your configuration sets a pause of #{SafePgMigrations.config.backfill_pause} seconds between batches of
23
+ #{SafePgMigrations.config.backfill_batch_size} rows. Each batch execution will take time as well. Please
24
+ check that the estimated duration of the migration is acceptable
25
+ before adding `safety_assured`.
26
+ CHECK
27
+
28
+ check_message += <<~CHECK if SafePgMigrations.config.default_value_backfill_threshold
29
+
30
+ Also, please note that SafePgMigrations is configured to raise if the table has more than
31
+ #{SafePgMigrations.config.default_value_backfill_threshold} rows.
32
+ CHECK
33
+
34
+ stop! check_message
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def strong_migration_available?
42
+ Object.const_defined? :StrongMigrations
43
+ end
44
+ end
45
+
46
+ SAFE_METHODS = %i[
47
+ execute
48
+ add_index
49
+ add_reference
50
+ add_belongs_to
51
+ change_column_null
52
+ add_foreign_key
53
+ add_check_constraint
54
+ ].freeze
55
+
56
+ SAFE_METHODS.each do |method|
57
+ define_method method do |*args|
58
+ return super(*args) unless respond_to?(:safety_assured)
59
+
60
+ safety_assured { super(*args) }
61
+ end
62
+ ruby2_keywords method
63
+ end
64
+
65
+ ruby2_keywords def add_column(table_name, *args)
66
+ return super(table_name, *args) unless respond_to?(:safety_assured)
67
+
68
+ options = args.last.is_a?(Hash) ? args.last : {}
69
+
70
+ return safety_assured { super(table_name, *args) } if options.fetch(:default_value_backfill, :auto) == :auto
71
+
72
+ super(table_name, *args)
73
+ end
74
+ end
75
+ end
@@ -4,8 +4,10 @@ module SafePgMigrations
4
4
  module UselessStatementsLogger
5
5
  class << self
6
6
  ruby2_keywords def warn_useless(action, link = nil, *args)
7
- SafePgMigrations.say "/!\\ No need to explicitly use #{action}, safe-pg-migrations does it for you", *args
8
- SafePgMigrations.say "\t see #{link} for more details", *args if link
7
+ Helpers::Logger.say(
8
+ "/!\\ No need to explicitly use #{action}, safe-pg-migrations does it for you", *args
9
+ )
10
+ Helpers::Logger.say "\t see #{link} for more details", *args if link
9
11
  end
10
12
  end
11
13
 
@@ -9,6 +9,8 @@ module SafePgMigrations
9
9
  ActiveRecord::Migration.prepend(SafePgMigrations::Migration)
10
10
  ActiveRecord::Migration.singleton_class.prepend(SafePgMigrations::Migration::ClassMethods)
11
11
  end
12
+
13
+ SafePgMigrations::StrongMigrationsIntegration.initialize
12
14
  end
13
15
  end
14
16
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafePgMigrations
4
- VERSION = '2.1.0'
4
+ VERSION = '2.2.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe-pg-migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu Prat
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-06-29 00:00:00.000000000 Z
13
+ date: 2023-07-07 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -55,12 +55,16 @@ files:
55
55
  - lib/safe-pg-migrations/helpers/blocking_activity_formatter.rb
56
56
  - lib/safe-pg-migrations/helpers/blocking_activity_selector.rb
57
57
  - lib/safe-pg-migrations/helpers/index_helper.rb
58
+ - lib/safe-pg-migrations/helpers/logger.rb
58
59
  - lib/safe-pg-migrations/helpers/satisfied_helper.rb
60
+ - lib/safe-pg-migrations/helpers/session_setting_management.rb
59
61
  - lib/safe-pg-migrations/plugins/blocking_activity_logger.rb
60
62
  - lib/safe-pg-migrations/plugins/idempotent_statements.rb
61
63
  - lib/safe-pg-migrations/plugins/statement_insurer.rb
62
64
  - lib/safe-pg-migrations/plugins/statement_insurer/add_column.rb
65
+ - lib/safe-pg-migrations/plugins/statement_insurer/change_column_null.rb
63
66
  - lib/safe-pg-migrations/plugins/statement_retrier.rb
67
+ - lib/safe-pg-migrations/plugins/strong_migrations_integration.rb
64
68
  - lib/safe-pg-migrations/plugins/useless_statements_logger.rb
65
69
  - lib/safe-pg-migrations/plugins/verbose_sql_logger.rb
66
70
  - lib/safe-pg-migrations/polyfills/index_definition_polyfill.rb