safe-pg-migrations 1.1.0 → 1.3.0

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: d806334c473708774d2180dbe91d2483b49f0d27b0a223a2973bc95d986d4030
4
- data.tar.gz: 9b5c2d329d5cc1bd12c80944420e20eb5454e5b0a0f7fda2d6ec3fc3c3a5e042
3
+ metadata.gz: fdee6e752da3fbdec93e5fd5c72fb370497ddd66f1cc27f33609c2a31e364c53
4
+ data.tar.gz: 143a5dcbf614abed765fac78b16969d900ded5ec574f11d939d802b94d7e2586
5
5
  SHA512:
6
- metadata.gz: 6786e68cce9dbd91a4633c80526a1fd63f6fbfa335d6e2b53d29b71ba0b566f9bb32169745bb13f89a059b82b0c0b21585a9bffdb9ebc751aa0e4a467f0f7934
7
- data.tar.gz: 299a69f1d9413279fd61908a7d8f6eb65bd4c9912abcc1101ca8acbfee39c40d33086efa28a9f2133c0679c1ad9cdbbfe3ce84338f2175ad882b7d1ace707654
6
+ metadata.gz: 41c264aa05c7b8ad5fe90f7ecdc88fa700504562e69695d5fd9f4dc9ac63315fb666818554e1387251b367aed241fccff31154589f70309905e1ed1cfae90c68
7
+ data.tar.gz: 9386e6a8b0366387543998ec5cec42e719d2a4138d5eb52f13f6580d2a3d31e48bd438760b358beb1ab5eb32bd8598927d69bafd03756108a514e88eeb431022
data/README.md CHANGED
@@ -16,6 +16,8 @@ Just drop this line in your Gemfile:
16
16
  gem 'safe-pg-migrations'
17
17
  ```
18
18
 
19
+ **Note: Do not run migrations via PgBouncer connection if it is configured to use transactional or statement pooling modes. You must run migrations via a direct Postgres connection, or configure PgBouncer to use session pooling mode.**
20
+
19
21
  ## Example
20
22
 
21
23
  Consider the following migration:
@@ -61,7 +63,7 @@ Under the hood, **Safe PG Migrations** patches `ActiveRecord::Migration` and ext
61
63
 
62
64
  ## Motivation
63
65
 
64
- Writing a safe migration can be daunting. Numerous articles have been written on the topic and a few gems are trying to address the problem. Even for someone who has a pretty good command of Postgres, remembering all the subtleties of explicit locking is not a piece of cake.
66
+ Writing a safe migration can be daunting. Numerous articles, [including ours](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9), have been written on the topic and a few gems are trying to address the problem. Even for someone who has a pretty good command of Postgres, remembering all the subtleties of explicit locking is not a piece of cake.
65
67
 
66
68
  Active Record means developers don't have to be proficient in SQL to interact with a database. In the same way, **Safe PG Migrations** was created so that developers don't have to understand the ins and outs of Postgres to write a safe migration.
67
69
 
@@ -122,7 +124,7 @@ Note: the addition of the not null constraint may timeout. In that case, you may
122
124
 
123
125
  </details>
124
126
 
125
- <details><summary>Safe <code>add_index</code> and <code>remove_index</code></summary>
127
+ <details><summary id="safe_add_remove_index">Safe <code>add_index</code> and <code>remove_index</code></summary>
126
128
 
127
129
  Creating an index requires a `SHARE` lock on the target table which blocks all write on the table while the index is created (which can take some time on a large table). This is usually not practical in a live environment. Thus, **Safe PG Migrations** ensures indexes are created concurrently.
128
130
 
@@ -135,7 +137,7 @@ If you still get lock timeout while adding / removing indexes, it might be for o
135
137
 
136
138
  </details>
137
139
 
138
- <details><summary>safe <code>add_foreign_key</code> (and <code>add_reference</code>)</summary>
140
+ <details><summary id="safe_add_foreign_key">safe <code>add_foreign_key</code> (and <code>add_reference</code>)</summary>
139
141
 
140
142
  Adding a foreign key requires a `SHARE ROW EXCLUSIVE` lock, which **prevent writing in the tables** while the migration is running.
141
143
 
@@ -206,9 +208,9 @@ So you can actually check that the `CREATE INDEX` statement will be performed co
206
208
  ```ruby
207
209
  SafePgMigrations.config.safe_timeout = 5.seconds # Lock and statement timeout used for all DDL operations except from CREATE / DROP INDEX
208
210
 
209
- SafePgMigrations.config.index_lock_timeout = 30.seconds # Lock timeout used for CREATE / DROP INDEX
211
+ SafePgMigrations.config.blocking_activity_logger_verbose = true # Outputs the raw blocking queries on timeout. When false, outputs information about the lock instead
210
212
 
211
- SafePgMigrations.config.blocking_activity_logger_margin = 1.second # Delay to output blocking queries before timeout. Must be smaller than safe_timeout and index_lock_timeout
213
+ SafePgMigrations.config.blocking_activity_logger_margin = 1.second # Delay to output blocking queries before timeout. Must be shorter than safe_timeout
212
214
 
213
215
  SafePgMigrations.config.batch_size = 1000 # Size of the batches used for backfilling when adding a column with a default value pre-PG11
214
216
 
@@ -254,3 +256,4 @@ Interesting reads:
254
256
  - [Rails migrations with no downtime](https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
255
257
  - [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/)
256
258
  - [Rails Migrations with Zero Downtime](https://blog.codeship.com/rails-migrations-zero-downtime/)
259
+ - [Stop worrying about PostgreSQL locks in your Rails migrations](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9)
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ruby2_keywords'
3
4
  require 'safe-pg-migrations/configuration'
4
5
  require 'safe-pg-migrations/plugins/verbose_sql_logger'
5
6
  require 'safe-pg-migrations/plugins/blocking_activity_logger'
6
7
  require 'safe-pg-migrations/plugins/statement_insurer'
7
8
  require 'safe-pg-migrations/plugins/statement_retrier'
8
9
  require 'safe-pg-migrations/plugins/idem_potent_statements'
10
+ require 'safe-pg-migrations/plugins/useless_statements_logger'
9
11
 
10
12
  module SafePgMigrations
11
13
  # Order matters: the bottom-most plugin will have precedence
@@ -14,6 +16,7 @@ module SafePgMigrations
14
16
  IdemPotentStatements,
15
17
  StatementRetrier,
16
18
  StatementInsurer,
19
+ UselessStatementsLogger,
17
20
  ].freeze
18
21
 
19
22
  class << self
@@ -48,13 +51,13 @@ module SafePgMigrations
48
51
  @alternate_connection = nil
49
52
  end
50
53
 
51
- def say(*args)
54
+ ruby2_keywords def say(*args)
52
55
  return unless current_migration
53
56
 
54
57
  current_migration.say(*args)
55
58
  end
56
59
 
57
- def say_method_call(method, *args)
60
+ ruby2_keywords def say_method_call(method, *args)
58
61
  say "#{method}(#{args.map(&:inspect) * ', '})", true
59
62
  end
60
63
 
@@ -78,6 +81,7 @@ module SafePgMigrations
78
81
  end
79
82
 
80
83
  def disable_ddl_transaction
84
+ UselessStatementsLogger.warn_useless '`disable_ddl_transaction`' if super
81
85
  true
82
86
  end
83
87
 
@@ -88,6 +92,7 @@ module SafePgMigrations
88
92
 
89
93
  safety_assured { super(*args) }
90
94
  end
95
+ ruby2_keywords method
91
96
  end
92
97
  end
93
98
  end
@@ -5,16 +5,16 @@ require 'active_support/core_ext/numeric/time'
5
5
  module SafePgMigrations
6
6
  class Configuration
7
7
  attr_accessor :safe_timeout
8
- attr_accessor :index_lock_timeout
9
8
  attr_accessor :blocking_activity_logger_margin
9
+ attr_accessor :blocking_activity_logger_verbose
10
10
  attr_accessor :batch_size
11
11
  attr_accessor :retry_delay
12
12
  attr_accessor :max_tries
13
13
 
14
14
  def initialize
15
15
  self.safe_timeout = 5.seconds
16
- self.index_lock_timeout = 30.seconds
17
16
  self.blocking_activity_logger_margin = 1.second
17
+ self.blocking_activity_logger_verbose = true
18
18
  self.batch_size = 1000
19
19
  self.retry_delay = 1.minute
20
20
  self.max_tries = 5
@@ -24,10 +24,6 @@ module SafePgMigrations
24
24
  pg_duration(safe_timeout)
25
25
  end
26
26
 
27
- def pg_index_lock_timeout
28
- pg_duration(index_lock_timeout)
29
- end
30
-
31
27
  def pg_duration(duration)
32
28
  value, unit = duration.integer? ? [duration, 's'] : [(duration * 1000).to_i, 'ms']
33
29
  "#{value}#{unit}"
@@ -2,55 +2,64 @@
2
2
 
3
3
  module SafePgMigrations
4
4
  module BlockingActivityLogger
5
- SELECT_BLOCKING_QUERIES_SQL = <<~SQL.squish
6
- SELECT blocking_activity.query
7
- FROM pg_catalog.pg_locks blocked_locks
8
- JOIN pg_catalog.pg_stat_activity blocked_activity
9
- ON blocked_activity.pid = blocked_locks.pid
10
- JOIN pg_catalog.pg_locks blocking_locks
11
- ON blocking_locks.locktype = blocked_locks.locktype
12
- AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
13
- AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
14
- AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
15
- AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
16
- AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
17
- AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
18
- AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
19
- AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
20
- AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
21
- AND blocking_locks.pid != blocked_locks.pid
22
- JOIN pg_catalog.pg_stat_activity blocking_activity
23
- ON blocking_activity.pid = blocking_locks.pid
24
- WHERE blocked_locks.pid = %d
25
- SQL
5
+ FILTERED_COLUMNS = %w[
6
+ blocked_activity.xact_start
7
+ blocked_locks.locktype
8
+ blocked_locks.mode
9
+ blocking_activity.pid
10
+ blocked_locks.transactionid
11
+ ].freeze
12
+
13
+ VERBOSE_COLUMNS = %w[
14
+ blocking_activity.query
15
+ blocked_activity.xact_start
16
+ ].freeze
26
17
 
27
18
  %i[
28
- add_column remove_column add_foreign_key remove_foreign_key change_column_default
29
- change_column_null create_table add_index remove_index
19
+ add_column remove_column add_foreign_key remove_foreign_key change_column_default change_column_null create_table
30
20
  ].each do |method|
31
21
  define_method method do |*args, &block|
32
- log_blocking_queries(method) { super(*args, &block) }
22
+ log_blocking_queries { super(*args, &block) }
33
23
  end
24
+ ruby2_keywords method
34
25
  end
35
26
 
36
27
  private
37
28
 
38
- def delay_before_logging(method)
39
- timeout_delay =
40
- if %i[add_index remove_index].include?(method)
41
- SafePgMigrations.config.index_lock_timeout
42
- else
43
- SafePgMigrations.config.safe_timeout
44
- end
29
+ def select_blocking_queries_sql
30
+ columns = SafePgMigrations.config.blocking_activity_logger_verbose ? VERBOSE_COLUMNS : FILTERED_COLUMNS
45
31
 
46
- timeout_delay - SafePgMigrations.config.blocking_activity_logger_margin
32
+ <<~SQL.squish
33
+ SELECT #{columns.join(', ')}
34
+ FROM pg_catalog.pg_locks blocked_locks
35
+ JOIN pg_catalog.pg_stat_activity blocked_activity
36
+ ON blocked_activity.pid = blocked_locks.pid
37
+ JOIN pg_catalog.pg_locks blocking_locks
38
+ ON blocking_locks.locktype = blocked_locks.locktype
39
+ AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
40
+ AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
41
+ AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
42
+ AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
43
+ AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
44
+ AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
45
+ AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
46
+ AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
47
+ AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
48
+ AND blocking_locks.pid != blocked_locks.pid
49
+ JOIN pg_catalog.pg_stat_activity blocking_activity
50
+ ON blocking_activity.pid = blocking_locks.pid
51
+ WHERE blocked_locks.pid = %d
52
+ SQL
47
53
  end
48
54
 
49
- def log_blocking_queries(method)
55
+ def log_blocking_queries
56
+ delay_before_logging =
57
+ SafePgMigrations.config.safe_timeout - SafePgMigrations.config.blocking_activity_logger_margin
58
+
50
59
  blocking_queries_retriever_thread =
51
60
  Thread.new do
52
- sleep delay_before_logging(method)
53
- SafePgMigrations.alternate_connection.query_values(SELECT_BLOCKING_QUERIES_SQL % raw_connection.backend_pid)
61
+ sleep delay_before_logging
62
+ SafePgMigrations.alternate_connection.query(select_blocking_queries_sql % raw_connection.backend_pid)
54
63
  end
55
64
 
56
65
  yield
@@ -75,7 +84,7 @@ module SafePgMigrations
75
84
  "Statement was being blocked by the following #{'query'.pluralize(queries.size)}:", true
76
85
  )
77
86
  SafePgMigrations.say '', true
78
- queries.each { |query| SafePgMigrations.say " #{query}", true }
87
+ output_blocking_queries(queries)
79
88
  SafePgMigrations.say(
80
89
  'Beware, some of those queries might run in a transaction. In this case the locking query might be '\
81
90
  'located elsewhere in the transaction',
@@ -86,5 +95,26 @@ module SafePgMigrations
86
95
 
87
96
  raise
88
97
  end
98
+
99
+ def output_blocking_queries(queries)
100
+ if SafePgMigrations.config.blocking_activity_logger_verbose
101
+ queries.each { |query, start_time| SafePgMigrations.say "#{format_start_time start_time}: #{query}", true }
102
+ else
103
+ queries.each do |start_time, locktype, mode, pid, transactionid|
104
+ SafePgMigrations.say(
105
+ "#{format_start_time(start_time)}: lock type: #{locktype || 'null'}, " \
106
+ "lock mode: #{mode || 'null'}, " \
107
+ "lock pid: #{pid || 'null'}, " \
108
+ "lock transactionid: #{transactionid || 'null'}",
109
+ true
110
+ )
111
+ end
112
+ end
113
+ end
114
+
115
+ def format_start_time(start_time, reference_time = Time.now)
116
+ duration = (reference_time - start_time).round
117
+ "transaction started #{duration} #{'second'.pluralize(duration)} ago"
118
+ end
89
119
  end
90
120
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module SafePgMigrations
4
4
  module IdemPotentStatements
5
- def add_index(table_name, column_name, **options)
5
+ ruby2_keywords def add_index(table_name, column_name, *args)
6
+ options = args.last.is_a?(Hash) ? args.last : {}
6
7
  index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, index_column_names(column_name))
7
8
  return super unless index_name_exists?(table_name, index_name)
8
9
 
@@ -12,19 +13,20 @@ module SafePgMigrations
12
13
  super
13
14
  end
14
15
 
15
- def add_column(table_name, column_name, type, options = {})
16
+ ruby2_keywords def add_column(table_name, column_name, type, *)
16
17
  return super unless column_exists?(table_name, column_name)
17
18
 
18
19
  SafePgMigrations.say("/!\\ Column '#{column_name}' already exists in '#{table_name}'. Skipping statement.", true)
19
20
  end
20
21
 
21
- def remove_column(table_name, column_name, type = nil, options = {})
22
+ ruby2_keywords def remove_column(table_name, column_name, type = nil, *)
22
23
  return super if column_exists?(table_name, column_name)
23
24
 
24
25
  SafePgMigrations.say("/!\\ Column '#{column_name}' not found on table '#{table_name}'. Skipping statement.", true)
25
26
  end
26
27
 
27
- def remove_index(table_name, options = {})
28
+ ruby2_keywords def remove_index(table_name, *args)
29
+ options = args.last.is_a?(Hash) ? args.last : {}
28
30
  index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, options)
29
31
 
30
32
  return super if index_name_exists?(table_name, index_name)
@@ -32,9 +34,10 @@ module SafePgMigrations
32
34
  SafePgMigrations.say("/!\\ Index '#{index_name}' not found on table '#{table_name}'. Skipping statement.", true)
33
35
  end
34
36
 
35
- def add_foreign_key(from_table, to_table, **options)
36
- options_or_to_table = options.slice(:name, :column).presence || to_table
37
- return super unless foreign_key_exists?(from_table, options_or_to_table)
37
+ ruby2_keywords def add_foreign_key(from_table, to_table, *args)
38
+ options = args.last.is_a?(Hash) ? args.last : {}
39
+ suboptions = options.slice(:name, :column)
40
+ return super unless foreign_key_exists?(from_table, suboptions.present? ? nil : to_table, **suboptions)
38
41
 
39
42
  SafePgMigrations.say(
40
43
  "/!\\ Foreign key '#{from_table}' -> '#{to_table}' already exists. Skipping statement.",
@@ -42,13 +45,21 @@ module SafePgMigrations
42
45
  )
43
46
  end
44
47
 
45
- def create_table(table_name, comment: nil, **options)
46
- return super unless table_exists?(table_name)
48
+ ruby2_keywords def create_table(table_name, *args)
49
+ options = args.last.is_a?(Hash) ? args.last : {}
50
+ return super if options[:force] || !table_exists?(table_name)
47
51
 
48
- SafePgMigrations.say(
49
- "/!\\ Table '#{table_name}' already exists. Skipping statement.",
50
- true
51
- )
52
+ SafePgMigrations.say "/!\\ Table '#{table_name}' already exists.", true
53
+
54
+ td = create_table_definition(table_name, *args)
55
+
56
+ yield td if block_given?
57
+
58
+ SafePgMigrations.say(td.indexes.empty? ? '-- Skipping statement' : '-- Creating indexes', true)
59
+
60
+ td.indexes.each do |column_name, index_options|
61
+ add_index(table_name, column_name, **index_options)
62
+ end
52
63
  end
53
64
 
54
65
  private
@@ -1,19 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafePgMigrations
4
- module StatementInsurer
4
+ module StatementInsurer # rubocop:disable Metrics/ModuleLength
5
5
  PG_11_VERSION_NUM = 110_000
6
6
 
7
- %i[change_column_null change_column create_table].each do |method|
7
+ %i[change_column_null change_column].each do |method|
8
8
  define_method method do |*args, &block|
9
9
  with_setting(:statement_timeout, SafePgMigrations.config.pg_safe_timeout) { super(*args, &block) }
10
10
  end
11
+ ruby2_keywords method
11
12
  end
12
13
 
13
- def add_column(table_name, column_name, type, **options) # rubocop:disable Metrics/CyclomaticComplexity
14
- need_default_value_backfill = SafePgMigrations.pg_version_num < PG_11_VERSION_NUM
14
+ ruby2_keywords def add_column(table_name, column_name, type, *args) # rubocop:disable Metrics/CyclomaticComplexity
15
+ options = args.last.is_a?(Hash) ? args.last : {}
16
+ return super if SafePgMigrations.pg_version_num >= PG_11_VERSION_NUM
15
17
 
16
- default = options.delete(:default) if need_default_value_backfill
18
+ default = options.delete(:default)
17
19
  null = options.delete(:null)
18
20
 
19
21
  if !default.nil? || null == false
@@ -22,7 +24,7 @@ module SafePgMigrations
22
24
 
23
25
  super
24
26
 
25
- if need_default_value_backfill && !default.nil?
27
+ unless default.nil?
26
28
  SafePgMigrations.say_method_call(:change_column_default, table_name, column_name, default)
27
29
  change_column_default(table_name, column_name, default)
28
30
 
@@ -36,29 +38,50 @@ module SafePgMigrations
36
38
  end
37
39
  end
38
40
 
39
- def add_foreign_key(from_table, to_table, **options)
41
+ ruby2_keywords def add_foreign_key(from_table, to_table, *args)
42
+ options = args.last.is_a?(Hash) ? args.last : {}
40
43
  validate_present = options.key? :validate
41
44
  options[:validate] = false unless validate_present
45
+ with_setting(:statement_timeout, SafePgMigrations.config.pg_safe_timeout) do
46
+ super(from_table, to_table, **options)
47
+ end
48
+
49
+ return if validate_present
42
50
 
43
- with_setting(:statement_timeout, SafePgMigrations.config.pg_safe_timeout) { super }
51
+ suboptions = options.slice(:name, :column)
52
+ without_statement_timeout { validate_foreign_key from_table, suboptions.present? ? nil : to_table, **suboptions }
53
+ end
44
54
 
45
- options_or_to_table = options.slice(:name, :column).presence || to_table
46
- without_statement_timeout { validate_foreign_key from_table, options_or_to_table } unless validate_present
55
+ ruby2_keywords def create_table(*)
56
+ with_setting(:statement_timeout, SafePgMigrations.config.pg_safe_timeout) do
57
+ super do |td|
58
+ yield td if block_given?
59
+ td.indexes.map! do |key, index_options|
60
+ index_options[:algorithm] ||= :default
61
+ [key, index_options]
62
+ end
63
+ end
64
+ end
47
65
  end
48
66
 
49
67
  def add_index(table_name, column_name, **options)
50
- options[:algorithm] = :concurrently
51
- SafePgMigrations.say_method_call(:add_index, table_name, column_name, options)
68
+ if options[:algorithm] == :default
69
+ options.delete :algorithm
70
+ else
71
+ options[:algorithm] = :concurrently
72
+ end
52
73
 
53
- with_index_timeouts { super }
74
+ SafePgMigrations.say_method_call(:add_index, table_name, column_name, **options)
75
+
76
+ without_timeout { super(table_name, column_name, **options) }
54
77
  end
55
78
 
56
- def remove_index(table_name, options = {})
57
- options = { column: options } unless options.is_a?(Hash)
58
- options[:algorithm] = :concurrently
59
- SafePgMigrations.say_method_call(:remove_index, table_name, options)
79
+ ruby2_keywords def remove_index(table_name, *args)
80
+ options = args.last.is_a?(Hash) ? args.last : { column: args.last }
81
+ options[:algorithm] = :concurrently unless options.key?(:algorithm)
82
+ SafePgMigrations.say_method_call(:remove_index, table_name, **options)
60
83
 
61
- with_index_timeouts { super }
84
+ without_timeout { super(table_name, **options) }
62
85
  end
63
86
 
64
87
  def backfill_column_default(table_name, column_name)
@@ -99,12 +122,12 @@ module SafePgMigrations
99
122
  with_setting(:statement_timeout, 0) { yield }
100
123
  end
101
124
 
102
- def with_index_timeouts
103
- without_statement_timeout do
104
- with_setting(:lock_timeout, SafePgMigrations.config.pg_index_lock_timeout) do
105
- yield
106
- end
107
- end
125
+ def without_lock_timeout
126
+ with_setting(:lock_timeout, 0) { yield }
127
+ end
128
+
129
+ def without_timeout
130
+ without_statement_timeout { without_lock_timeout { yield } }
108
131
  end
109
132
  end
110
133
  end
@@ -3,14 +3,14 @@
3
3
  module SafePgMigrations
4
4
  module StatementRetrier
5
5
  RETRIABLE_SCHEMA_STATEMENTS = %i[
6
- add_column add_foreign_key remove_foreign_key change_column_default
7
- change_column_null add_index remove_index remove_column
6
+ add_column add_foreign_key remove_foreign_key change_column_default change_column_null remove_column
8
7
  ].freeze
9
8
 
10
9
  RETRIABLE_SCHEMA_STATEMENTS.each do |method|
11
10
  define_method method do |*args, &block|
12
11
  retry_if_lock_timeout { super(*args, &block) }
13
12
  end
13
+ ruby2_keywords method
14
14
  end
15
15
 
16
16
  private
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module UselessStatementsLogger
5
+ class << self
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
9
+ end
10
+ end
11
+
12
+ ruby2_keywords def add_index(*args)
13
+ options = args.last.is_a?(Hash) ? args.last : {}
14
+ warn_for_index(**options)
15
+ super
16
+ end
17
+
18
+ ruby2_keywords def remove_index(table_name, *args)
19
+ options = args.last.is_a?(Hash) ? args.last : {}
20
+ warn_for_index(**options) unless options.empty?
21
+ super
22
+ end
23
+
24
+ ruby2_keywords def add_foreign_key(*args)
25
+ options = args.last.is_a?(Hash) ? args.last : {}
26
+ if options[:validate] == false
27
+ UselessStatementsLogger.warn_useless '`validate: :false`', 'https://github.com/doctolib/safe-pg-migrations#safe_add_foreign_key'
28
+ end
29
+ super
30
+ end
31
+
32
+ def warn_for_index(**options)
33
+ return unless options[:algorithm] == :concurrently
34
+
35
+ UselessStatementsLogger.warn_useless '`algorithm: :concurrently`', 'https://github.com/doctolib/safe-pg-migrations#safe_add_remove_index'
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafePgMigrations
4
- VERSION = '1.1.0'
4
+ VERSION = '1.3.0'
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: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu Prat
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-11-03 00:00:00.000000000 Z
12
+ date: 2021-06-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -39,6 +39,20 @@ dependencies:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
41
  version: '5.2'
42
+ - !ruby/object:Gem::Dependency
43
+ name: ruby2_keywords
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 0.0.4
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 0.0.4
42
56
  - !ruby/object:Gem::Dependency
43
57
  name: bundler
44
58
  requirement: !ruby/object:Gem::Requirement
@@ -166,6 +180,7 @@ files:
166
180
  - lib/safe-pg-migrations/plugins/idem_potent_statements.rb
167
181
  - lib/safe-pg-migrations/plugins/statement_insurer.rb
168
182
  - lib/safe-pg-migrations/plugins/statement_retrier.rb
183
+ - lib/safe-pg-migrations/plugins/useless_statements_logger.rb
169
184
  - lib/safe-pg-migrations/plugins/verbose_sql_logger.rb
170
185
  - lib/safe-pg-migrations/railtie.rb
171
186
  - lib/safe-pg-migrations/version.rb
@@ -182,6 +197,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
182
197
  - - ">="
183
198
  - !ruby/object:Gem::Version
184
199
  version: '2.5'
200
+ - - "<"
201
+ - !ruby/object:Gem::Version
202
+ version: '4'
185
203
  required_rubygems_version: !ruby/object:Gem::Requirement
186
204
  requirements:
187
205
  - - ">="