safe-pg-migrations 0.0.2 → 1.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
- SHA1:
3
- metadata.gz: e766b867ff3d15f71bdaaa161e595e51bd1b83b9
4
- data.tar.gz: b81ced0f60a21fe8e811962a614fc913ad97349a
2
+ SHA256:
3
+ metadata.gz: fbda5becc48b8f9c7e0f45820f5f9ef908ba1f1fe56c306f03d3f76fb0197741
4
+ data.tar.gz: 4e88da86831d71cb440e1409ad52f3591aabcbd26de248507daffbec0e17698d
5
5
  SHA512:
6
- metadata.gz: cd45ef3561909949c81b0b9927ed8e3f2b58ab101fd5436f8cb11afb72696e0668bb1bc773c8d66693f07c17456ce9b75f046f20866d1780165f96fd1e17f4a5
7
- data.tar.gz: f0fca599eccc2ebb909c168ff9509e6f04ef6b16853aa4ae881a1274775b271f37881fe13c12edc4b03d4c7630d69010e3ef6f2401e861326c9380b8db588ef6
6
+ metadata.gz: e7dfbc18db1959f66a104e37d1f880934b369338b96435efc2db851fe80c092e9cf1e19a092274726dcb35da2fb05035d71a63e404dc89366de6c3483ce2131f
7
+ data.tar.gz: a176ae5e044b31ef271359aa9342698e1744e00c3fe99eb8f10c65fa77127671bf5265f4a4e65117df2f22bbc01bbc201ea22ecd2e0faca36e0822cc2f85e2b9
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # safe-pg-migrations [![Build Status](https://travis-ci.org/doctolib/safe-pg-migrations.svg?branch=master)](https://travis-ci.org/doctolib/safe-pg-migrations)
1
+ # safe-pg-migrations
2
2
 
3
3
  ActiveRecord migrations for Postgres made safe.
4
4
 
5
5
  ## Requirements
6
6
 
7
- - Ruby 2.3+
7
+ - Ruby 2.5+
8
8
  - Rails 5.2+
9
9
  - PostgreSQL 9.3+
10
10
 
@@ -28,7 +28,7 @@ class AddAdminToUsers < ActiveRecord::Migration[5.2]
28
28
  end
29
29
  ```
30
30
 
31
- If the `users` table is large, running this migration on a live Postgres database will likely cause downtime. **Safe PG Migrations** hooks into Active Record so that the following gets executed instead:
31
+ If the `users` table is large, running this migration on a live Postgres 9 database will likely cause downtime. **Safe PG Migrations** hooks into Active Record so that the following gets executed instead:
32
32
 
33
33
  ```rb
34
34
  class AddAdminToUsers < ActiveRecord::Migration[5.2]
@@ -67,21 +67,23 @@ Active Record means developers don't have to be proficient in SQL to interact wi
67
67
 
68
68
  ## Feature set
69
69
 
70
- ### Lock timeout
70
+ <details><summary>Lock timeout</summary>
71
71
 
72
- Most DDL operations (e.g. adding a column, removing a column or adding a default value to a column) take an `ACCESS EXCLUSIVE` lock on the table they are altering. While these operations wait to acquire their lock, other statements are blocked. Before running a migration, **Safe PG Migrations** sets a short lock timeout so that statements are not blocked for too long.
72
+ Most DDL operations (e.g. adding a column, removing a column or adding a default value to a column) take an `ACCESS EXCLUSIVE` lock on the table they are altering. While these operations wait to acquire their lock, other statements are blocked. Before running a migration, **Safe PG Migrations** sets a short lock timeout (default to 5 seconds) so that statements are not blocked for too long.
73
73
 
74
74
  See [PostgreSQL Alter Table and Long Transactions](http://www.joshuakehn.com/2017/9/9/postgresql-alter-table-and-long-transactions.html) and [Migrations and Long Transactions](https://www.fin.com/post/2018/1/migrations-and-long-transactions) for detailed explanations of the matter.
75
+ </details>
75
76
 
76
- ### Statement timeout
77
+ <details><summary>Statement timeout</summary>
77
78
 
78
- Adding a foreign key or a not-null constraint can take a lot of time on a large table. The problem is that those operations take `ACCESS EXCLUSIVE` locks. We clearly don't want them to hold these locks for too long. Thus, **Safe PG Migrations** runs them with a short statement timeout.
79
+ Adding a foreign key or a not-null constraint can take a lot of time on a large table. The problem is that those operations take `ACCESS EXCLUSIVE` locks. We clearly don't want them to hold these locks for too long. Thus, **Safe PG Migrations** runs them with a short statement timeout (default to 5 seconds).
79
80
 
80
81
  See [Zero-downtime Postgres migrations - the hard parts](https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts/) for a detailed explanation on the subject.
82
+ </details>
81
83
 
82
- ### Prevent wrapping migrations in transaction
84
+ <details><summary>Prevent wrapping migrations in transaction</summary>
83
85
 
84
- When **Safe PG Migrations** is enabled (which is the case by default if `Rails.env.production?` is true), migrations are not wrapped in a transaction. This is for several reasons:
86
+ When **Safe PG Migrations** is used, migrations are not wrapped in a transaction. This is for several reasons:
85
87
 
86
88
  - We want to release locks as soon as possible.
87
89
  - In order to be able to retry statements that have failed because of a lock timeout, we have to be outside a transaction.
@@ -89,8 +91,22 @@ When **Safe PG Migrations** is enabled (which is the case by default if `Rails.e
89
91
 
90
92
  Note that if a migration fails, it won't be rollbacked. This can result in migrations being partially applied. In that case, they need to be manually reverted.
91
93
 
92
- ### Safe `add_column`
94
+ </details>
93
95
 
96
+ <details>
97
+ <summary>Safe <code>add_column</code></summary>
98
+
99
+ **Safe PG Migrations** gracefully handle the upgrade to PG11 by **not** backfilling default value for existing rows, as the [database engine is now natively handling it](https://www.postgresql.org/docs/11/ddl-alter.html#DDL-ALTER-ADDING-A-COLUMN).
100
+
101
+ Beware though, when adding a volatile default value:
102
+ ```ruby
103
+ add_column :users, :created_at, default: 'clock_timestamp()'
104
+ ```
105
+ PG will still needs to update every row of the table, and will most likely statement timeout for big table. In this case, your best bet is to add the column without default, set the default, and backfill existing rows.
106
+
107
+ <blockquote>
108
+
109
+ **Note: Pre-postgre 11**
94
110
  Adding a column with a default value and a not-null constraint is [dangerous](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/).
95
111
 
96
112
  **Safe PG Migrations** makes it safe by:
@@ -102,23 +118,108 @@ Adding a column with a default value and a not-null constraint is [dangerous](ht
102
118
 
103
119
  Note: the addition of the not null constraint may timeout. In that case, you may want to add the not-null constraint as initially not valid and validate it in a separate statement. See [Adding a not-null constraint on Postgres with minimal locking](https://medium.com/doctolib-engineering/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c).
104
120
 
105
- ### Concurrent indexes
121
+ </blockquote>
122
+
123
+ </details>
124
+
125
+ <details><summary id="safe_add_remove_index">Safe <code>add_index</code> and <code>remove_index</code></summary>
106
126
 
107
127
  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.
108
128
 
109
- ### Retry after lock timeout
129
+ As `CREATE INDEX CONCURRENTLY` and `DROP INDEX CONCURRENTLY` are non-blocking operations (ie: read/write operations on the table are still possible), **Safe PG Migrations** sets a lock timeout to 30 seconds for those 2 specific statements.
130
+
131
+ If you still get lock timeout while adding / removing indexes, it might be for one of those reasons:
132
+
133
+ - Long-running queries are active on the table. To create / remove an index, PG needs to wait for the queries that are actually running to finish before starting the index creation / removal. The blocking activity logger might help you to pinpoint the culprit queries.
134
+ - A vacuum / autovacuum is running on the table, holding a ShareUpdateExclusiveLock, you are most likely out of luck for the current migration, but you may try to [optimize your autovacuums settings](https://www.percona.com/blog/2018/08/10/tuning-autovacuum-in-postgresql-and-autovacuum-internals/).
110
135
 
111
- When a statement fails with a lock timeout, **Safe PG Migrations** retries them (5 times max).
136
+ </details>
112
137
 
113
- ### Blocking activity logging
138
+ <details><summary id="safe_add_foreign_key">safe <code>add_foreign_key</code> (and <code>add_reference</code>)</summary>
139
+
140
+ Adding a foreign key requires a `SHARE ROW EXCLUSIVE` lock, which **prevent writing in the tables** while the migration is running.
141
+
142
+ Adding the constraint itself is rather fast, the major part of the time is spent on validating this constraint. Thus safe-pg-migrations ensures that adding a foreign key holds blocking locks for the least amount of time by splitting the foreign key creation in two steps:
143
+
144
+ 1. adding the constraint *without validation*, will not validate existing rows;
145
+ 2. validating the constraint, will validate existing rows in the table, without blocking read or write on the table
146
+
147
+ </details>
148
+
149
+ <details><summary>Retry after lock timeout</summary>
150
+
151
+ When a statement fails with a lock timeout, **Safe PG Migrations** retries it (5 times max) [list of retryable statments](https://github.com/doctolib/safe-pg-migrations/blob/66933256252b6bbf12e404b829a361dbba30e684/lib/safe-pg-migrations/plugins/statement_retrier.rb#L5)
152
+ </details>
153
+
154
+ <details><summary>Blocking activity logging</summary>
114
155
 
115
156
  If a statement fails with a lock timeout, **Safe PG Migrations** will try to tell you what was the blocking statement.
157
+ </details>
158
+
159
+ <details><summary>Verbose SQL logging</summary>
160
+
161
+ For any operation, **Safe PG Migrations** can output the performed SQL queries. This feature is enabled by default in a production Rails environment. If you want to explicit enable it, for example in your development environment you can use:
162
+ ```bash
163
+ export SAFE_PG_MIGRATIONS_VERBOSE=1
164
+ ```
165
+
166
+ Instead of the traditional output:
167
+ ```ruby
168
+ add_index :users, :age
169
+
170
+ == 20191215132355 SampleIndex: migrating ======================================
171
+ -- add_index(:users, :age)
172
+ -> add_index("users", :age, {:algorithm=>:concurrently})
173
+ -> 0.0175s
174
+ == 20191215132355 SampleIndex: migrated (0.0200s) =============================
175
+ ```
176
+ **Safe PG Migrations** will output the following logs:
177
+ ```ruby
178
+ add_index :users, :age
179
+
180
+ == 20191215132355 SampleIndex: migrating ======================================
181
+ (0.3ms) SHOW lock_timeout
182
+ (0.3ms) SET lock_timeout TO '5s'
183
+ -- add_index(:users, :age)
184
+ -> add_index("users", :age, {:algorithm=>:concurrently})
185
+ (0.3ms) SHOW statement_timeout
186
+ (0.3ms) SET statement_timeout TO 0
187
+ (0.3ms) SHOW lock_timeout
188
+ (0.3ms) SET lock_timeout TO '30s'
189
+ (3.5ms) CREATE INDEX CONCURRENTLY "index_users_on_age" ON "users" ("age")
190
+ (0.3ms) SET lock_timeout TO '5s'
191
+ (0.2ms) SET statement_timeout TO '1min'
192
+ -> 0.0093s
193
+ (0.2ms) SET lock_timeout TO '0'
194
+ == 20191215132355 SampleIndex: migrated (0.0114s) =============================
195
+ ```
196
+ So you can actually check that the `CREATE INDEX` statement will be performed concurrently, without any statement timeout and with a lock timeout of 30 seconds.
197
+
198
+ *Nb: The `SHOW` statements are used by **Safe PG Migrations** to query settings for their original values in order to restore them after the work is done*
199
+
200
+ </details>
201
+
202
+ ## Configuration
203
+
204
+ **Safe PG Migrations** can be customized, here is an example of a Rails initializer (the values are the default ones):
205
+
206
+ ```ruby
207
+ SafePgMigrations.config.safe_timeout = 5.seconds # Lock and statement timeout used for all DDL operations except from CREATE / DROP INDEX
208
+
209
+ SafePgMigrations.config.blocking_activity_logger_margin = 1.second # Delay to output blocking queries before timeout. Must be shorter than safe_timeout
210
+
211
+ SafePgMigrations.config.batch_size = 1000 # Size of the batches used for backfilling when adding a column with a default value pre-PG11
212
+
213
+ SafePgMigrations.config.retry_delay = 1.minute # Delay between retries for retryable statements
214
+
215
+ SafePgMigrations.config.max_tries = 5 # Number of retries before abortion of the migration
216
+ ```
116
217
 
117
218
  ## Runnings tests
118
219
 
119
220
  ```bash
120
221
  bundle
121
- psql -h localhost -c 'CREATE DATABASE safe_pg_migrations_test'
222
+ psql -h localhost -U postgres -c 'CREATE DATABASE safe_pg_migrations_test'
122
223
  rake test
123
224
  ```
124
225
 
@@ -126,6 +227,7 @@ rake test
126
227
 
127
228
  - [Matthieu Prat](https://github.com/matthieuprat)
128
229
  - [Romain Choquet](https://github.com/rchoquet)
230
+ - [Paul-Etienne Coisne](https://github.com/coisnepe)
129
231
 
130
232
  ## License
131
233
 
@@ -141,11 +243,12 @@ Alternatives:
141
243
 
142
244
  Interesting reads:
143
245
 
144
- - https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/
145
- - https://www.fin.com/post/2018/1/migrations-and-long-transactions
146
- - http://www.joshuakehn.com/2017/9/9/postgresql-alter-table-and-long-transactions.html
147
- - https://medium.com/doctolib-engineering/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c
148
- - https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/
149
- - https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/
150
- - https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/
151
- - https://blog.codeship.com/rails-migrations-zero-downtime/
246
+ - [When Postgres blocks: 7 tips for dealing with locks](https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/)
247
+ - [Migrations and Long Transactions in Postgres
248
+ ](https://www.fin.com/post/2018/1/migrations-and-long-transactions)
249
+ - [PostgreSQL Alter Table and Long Transactions](http://www.joshuakehn.com/2017/9/9/postgresql-alter-table-and-long-transactions.html)
250
+ - [Adding a NOT NULL CONSTRAINT on PG Faster with Minimal Locking](https://medium.com/doctolib-engineering/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c)
251
+ - [Adding columns with default values to really large tables in Postgres + Rails](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/)
252
+ - [Rails migrations with no downtime](https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
253
+ - [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/)
254
+ - [Rails Migrations with Zero Downtime](https://blog.codeship.com/rails-migrations-zero-downtime/)
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'safe-pg-migrations/configuration'
4
+ require 'safe-pg-migrations/plugins/verbose_sql_logger'
4
5
  require 'safe-pg-migrations/plugins/blocking_activity_logger'
5
6
  require 'safe-pg-migrations/plugins/statement_insurer'
6
7
  require 'safe-pg-migrations/plugins/statement_retrier'
7
8
  require 'safe-pg-migrations/plugins/idem_potent_statements'
9
+ require 'safe-pg-migrations/plugins/useless_statements_logger'
8
10
 
9
11
  module SafePgMigrations
10
12
  # Order matters: the bottom-most plugin will have precedence
@@ -13,20 +15,28 @@ module SafePgMigrations
13
15
  IdemPotentStatements,
14
16
  StatementRetrier,
15
17
  StatementInsurer,
18
+ UselessStatementsLogger,
16
19
  ].freeze
17
20
 
18
21
  class << self
19
- attr_reader :current_migration
20
- attr_accessor :enabled
22
+ attr_reader :current_migration, :pg_version_num
21
23
 
22
24
  def setup_and_teardown(migration, connection)
25
+ @pg_version_num = get_pg_version_num(connection)
23
26
  @alternate_connection = nil
24
27
  @current_migration = migration
28
+ stdout_sql_logger = VerboseSqlLogger.new.setup if verbose?
25
29
  PLUGINS.each { |plugin| connection.extend(plugin) }
26
- connection.with_setting(:lock_timeout, SafePgMigrations.config.safe_timeout) { yield }
30
+
31
+ connection.with_setting(:lock_timeout, SafePgMigrations.config.pg_safe_timeout) { yield }
27
32
  ensure
28
33
  close_alternate_connection
29
34
  @current_migration = nil
35
+ stdout_sql_logger&.teardown
36
+ end
37
+
38
+ def get_pg_version_num(connection)
39
+ connection.query_value('SHOW server_version_num').to_i
30
40
  end
31
41
 
32
42
  def alternate_connection
@@ -50,9 +60,8 @@ module SafePgMigrations
50
60
  say "#{method}(#{args.map(&:inspect) * ', '})", true
51
61
  end
52
62
 
53
- def enabled?
54
- return ENV['SAFE_PG_MIGRATIONS'] == '1' if ENV['SAFE_PG_MIGRATIONS']
55
- return enabled unless enabled.nil?
63
+ def verbose?
64
+ return ENV['SAFE_PG_MIGRATIONS_VERBOSE'] == '1' if ENV['SAFE_PG_MIGRATIONS_VERBOSE']
56
65
  return Rails.env.production? if defined?(Rails)
57
66
 
58
67
  false
@@ -71,7 +80,8 @@ module SafePgMigrations
71
80
  end
72
81
 
73
82
  def disable_ddl_transaction
74
- SafePgMigrations.enabled? || super
83
+ UselessStatementsLogger.warn_useless '`disable_ddl_transaction`' if super
84
+ true
75
85
  end
76
86
 
77
87
  SAFE_METHODS = %i[execute add_column add_index add_reference add_belongs_to change_column_null].freeze
@@ -5,17 +5,26 @@ require 'active_support/core_ext/numeric/time'
5
5
  module SafePgMigrations
6
6
  class Configuration
7
7
  attr_accessor :safe_timeout
8
- attr_accessor :blocking_activity_logger_delay # Must be close to but smaller than safe_timeout.
8
+ attr_accessor :blocking_activity_logger_margin
9
9
  attr_accessor :batch_size
10
10
  attr_accessor :retry_delay
11
11
  attr_accessor :max_tries
12
12
 
13
13
  def initialize
14
- self.safe_timeout = '5s'
15
- self.blocking_activity_logger_delay = 4.seconds
14
+ self.safe_timeout = 5.seconds
15
+ self.blocking_activity_logger_margin = 1.second
16
16
  self.batch_size = 1000
17
- self.retry_delay = 2.minutes
17
+ self.retry_delay = 1.minute
18
18
  self.max_tries = 5
19
19
  end
20
+
21
+ def pg_safe_timeout
22
+ pg_duration(safe_timeout)
23
+ end
24
+
25
+ def pg_duration(duration)
26
+ value, unit = duration.integer? ? [duration, 's'] : [(duration * 1000).to_i, 'ms']
27
+ "#{value}#{unit}"
28
+ end
20
29
  end
21
30
  end
@@ -3,7 +3,7 @@
3
3
  module SafePgMigrations
4
4
  module BlockingActivityLogger
5
5
  SELECT_BLOCKING_QUERIES_SQL = <<~SQL.squish
6
- SELECT blocking_activity.query
6
+ SELECT blocking_activity.query, blocked_activity.xact_start as start
7
7
  FROM pg_catalog.pg_locks blocked_locks
8
8
  JOIN pg_catalog.pg_stat_activity blocked_activity
9
9
  ON blocked_activity.pid = blocked_locks.pid
@@ -25,8 +25,7 @@ module SafePgMigrations
25
25
  SQL
26
26
 
27
27
  %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
28
+ add_column remove_column add_foreign_key remove_foreign_key change_column_default change_column_null create_table
30
29
  ].each do |method|
31
30
  define_method method do |*args, &block|
32
31
  log_blocking_queries { super(*args, &block) }
@@ -36,10 +35,13 @@ module SafePgMigrations
36
35
  private
37
36
 
38
37
  def log_blocking_queries
38
+ delay_before_logging =
39
+ SafePgMigrations.config.safe_timeout - SafePgMigrations.config.blocking_activity_logger_margin
40
+
39
41
  blocking_queries_retriever_thread =
40
42
  Thread.new do
41
- sleep SafePgMigrations.config.blocking_activity_logger_delay
42
- SafePgMigrations.alternate_connection.query_values(SELECT_BLOCKING_QUERIES_SQL % raw_connection.backend_pid)
43
+ sleep delay_before_logging
44
+ SafePgMigrations.alternate_connection.query(SELECT_BLOCKING_QUERIES_SQL % raw_connection.backend_pid)
43
45
  end
44
46
 
45
47
  yield
@@ -64,11 +66,21 @@ module SafePgMigrations
64
66
  "Statement was being blocked by the following #{'query'.pluralize(queries.size)}:", true
65
67
  )
66
68
  SafePgMigrations.say '', true
67
- queries.each { |query| SafePgMigrations.say " #{query}", true }
69
+ queries.each { |query, start_time| SafePgMigrations.say "#{format_start_time start_time}: #{query}", true }
70
+ SafePgMigrations.say(
71
+ 'Beware, some of those queries might run in a transaction. In this case the locking query might be '\
72
+ 'located elsewhere in the transaction',
73
+ true
74
+ )
68
75
  SafePgMigrations.say '', true
69
76
  end
70
77
 
71
78
  raise
72
79
  end
80
+
81
+ def format_start_time(start_time, reference_time = Time.now)
82
+ duration = (reference_time - start_time).round
83
+ "transaction started #{duration} #{'second'.pluralize(duration)} ago"
84
+ end
73
85
  end
74
86
  end
@@ -12,6 +12,52 @@ module SafePgMigrations
12
12
  super
13
13
  end
14
14
 
15
+ def add_column(table_name, column_name, type, options = {})
16
+ return super unless column_exists?(table_name, column_name)
17
+
18
+ SafePgMigrations.say("/!\\ Column '#{column_name}' already exists in '#{table_name}'. Skipping statement.", true)
19
+ end
20
+
21
+ def remove_column(table_name, column_name, type = nil, options = {})
22
+ return super if column_exists?(table_name, column_name)
23
+
24
+ SafePgMigrations.say("/!\\ Column '#{column_name}' not found on table '#{table_name}'. Skipping statement.", true)
25
+ end
26
+
27
+ def remove_index(table_name, options = {})
28
+ index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, options)
29
+
30
+ return super if index_name_exists?(table_name, index_name)
31
+
32
+ SafePgMigrations.say("/!\\ Index '#{index_name}' not found on table '#{table_name}'. Skipping statement.", true)
33
+ end
34
+
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)
38
+
39
+ SafePgMigrations.say(
40
+ "/!\\ Foreign key '#{from_table}' -> '#{to_table}' already exists. Skipping statement.",
41
+ true
42
+ )
43
+ end
44
+
45
+ def create_table(table_name, comment: nil, **options)
46
+ return super if options[:force] || !table_exists?(table_name)
47
+
48
+ SafePgMigrations.say "/!\\ Table '#{table_name}' already exists.", true
49
+
50
+ td = create_table_definition(table_name, **options)
51
+
52
+ yield td if block_given?
53
+
54
+ SafePgMigrations.say(td.indexes.empty? ? '-- Skipping statement' : '-- Creating indexes', true)
55
+
56
+ td.indexes.each do |column_name, index_options|
57
+ add_index(table_name, column_name, index_options)
58
+ end
59
+ end
60
+
15
61
  private
16
62
 
17
63
  def index_valid?(index_name)
@@ -2,23 +2,27 @@
2
2
 
3
3
  module SafePgMigrations
4
4
  module StatementInsurer
5
- %i[change_column_null add_foreign_key create_table].each do |method|
5
+ PG_11_VERSION_NUM = 110_000
6
+
7
+ %i[change_column_null change_column].each do |method|
6
8
  define_method method do |*args, &block|
7
- with_setting(:statement_timeout, SafePgMigrations.config.safe_timeout) { super(*args, &block) }
9
+ with_setting(:statement_timeout, SafePgMigrations.config.pg_safe_timeout) { super(*args, &block) }
8
10
  end
9
11
  end
10
12
 
11
- def add_column(table_name, column_name, type, **options)
12
- default = options.delete(:default)
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
15
+
16
+ default = options.delete(:default) if need_default_value_backfill
13
17
  null = options.delete(:null)
14
18
 
15
19
  if !default.nil? || null == false
16
- SafePgMigrations.say_method_call(:add_column, table_name, column_name, type, **options)
20
+ SafePgMigrations.say_method_call(:add_column, table_name, column_name, type, options)
17
21
  end
18
22
 
19
23
  super
20
24
 
21
- unless default.nil?
25
+ if need_default_value_backfill && !default.nil?
22
26
  SafePgMigrations.say_method_call(:change_column_default, table_name, column_name, default)
23
27
  change_column_default(table_name, column_name, default)
24
28
 
@@ -32,21 +36,46 @@ module SafePgMigrations
32
36
  end
33
37
  end
34
38
 
39
+ def add_foreign_key(from_table, to_table, **options)
40
+ validate_present = options.key? :validate
41
+ options[:validate] = false unless validate_present
42
+
43
+ with_setting(:statement_timeout, SafePgMigrations.config.pg_safe_timeout) { super }
44
+
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
47
+ end
48
+
49
+ def create_table(*)
50
+ with_setting(:statement_timeout, SafePgMigrations.config.pg_safe_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
57
+ end
58
+ end
59
+ end
60
+
35
61
  def add_index(table_name, column_name, **options)
36
- if SafePgMigrations.enabled?
62
+ if options[:algorithm] == :default
63
+ options.delete :algorithm
64
+ else
37
65
  options[:algorithm] = :concurrently
38
- SafePgMigrations.say_method_call(:add_index, table_name, column_name, **options)
39
66
  end
40
- without_statement_timeout { super }
67
+
68
+ SafePgMigrations.say_method_call(:add_index, table_name, column_name, options)
69
+
70
+ without_timeout { super }
41
71
  end
42
72
 
43
73
  def remove_index(table_name, options = {})
44
74
  options = { column: options } unless options.is_a?(Hash)
45
- if SafePgMigrations.enabled?
46
- options[:algorithm] = :concurrently
47
- SafePgMigrations.say_method_call(:remove_index, table_name, **options)
48
- end
49
- without_statement_timeout { super }
75
+ options[:algorithm] = :concurrently
76
+ SafePgMigrations.say_method_call(:remove_index, table_name, options)
77
+
78
+ without_timeout { super }
50
79
  end
51
80
 
52
81
  def backfill_column_default(table_name, column_name)
@@ -86,5 +115,13 @@ module SafePgMigrations
86
115
  def without_statement_timeout
87
116
  with_setting(:statement_timeout, 0) { yield }
88
117
  end
118
+
119
+ def without_lock_timeout
120
+ with_setting(:lock_timeout, 0) { yield }
121
+ end
122
+
123
+ def without_timeout
124
+ without_statement_timeout { without_lock_timeout { yield } }
125
+ end
89
126
  end
90
127
  end
@@ -3,8 +3,7 @@
3
3
  module SafePgMigrations
4
4
  module StatementRetrier
5
5
  RETRIABLE_SCHEMA_STATEMENTS = %i[
6
- add_column remove_column add_foreign_key remove_foreign_key change_column_default
7
- change_column_null add_index
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|
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module UselessStatementsLogger
5
+ def self.warn_useless(action, link = nil, *args)
6
+ SafePgMigrations.say "/!\\ No need to explicitly use #{action}, safe-pg-migrations does it for you", *args
7
+ SafePgMigrations.say "\t see #{link} for more details", *args if link
8
+ end
9
+
10
+ def add_index(*, **options)
11
+ warn_for_index(**options)
12
+ super
13
+ end
14
+
15
+ def remove_index(table_name, options = {})
16
+ warn_for_index(options) if options.is_a? Hash
17
+ super
18
+ end
19
+
20
+ def add_foreign_key(*, **options)
21
+ if options[:validate] == false
22
+ UselessStatementsLogger.warn_useless '`validate: :false`', 'https://github.com/doctolib/safe-pg-migrations#safe_add_foreign_key'
23
+ end
24
+ super
25
+ end
26
+
27
+ def warn_for_index(**options)
28
+ return unless options[:algorithm] == :concurrently
29
+
30
+ UselessStatementsLogger.warn_useless '`algorithm: :concurrently`', 'https://github.com/doctolib/safe-pg-migrations#safe_add_remove_index'
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ class VerboseSqlLogger
5
+ def setup
6
+ @activerecord_logger_was = ActiveRecord::Base.logger
7
+ @verbose_query_logs_was = ActiveRecord::Base.verbose_query_logs
8
+ @colorize_logging_was = ActiveRecord::LogSubscriber.colorize_logging
9
+
10
+ disable_marginalia if defined?(Marginalia)
11
+
12
+ stdout_logger = Logger.new($stdout, formatter: ->(_severity, _time, _progname, query) { "#{query}\n" })
13
+ ActiveRecord::Base.logger = stdout_logger
14
+ ActiveRecord::LogSubscriber.colorize_logging = colorize_logging?
15
+ # Do not output caller method, we know it is coming from the migration
16
+ ActiveRecord::Base.verbose_query_logs = false
17
+ self
18
+ end
19
+
20
+ def teardown
21
+ ActiveRecord::Base.verbose_query_logs = @verbose_query_logs_was
22
+ ActiveRecord::LogSubscriber.colorize_logging = @colorize_logging_was
23
+ ActiveRecord::Base.logger = @activerecord_logger_was
24
+ enable_marginalia if defined?(Marginalia)
25
+ end
26
+
27
+ private
28
+
29
+ def colorize_logging?
30
+ defined?(Rails) && Rails.env.development?
31
+ end
32
+
33
+ # Marginalia annotations will most likely pollute the output
34
+ def disable_marginalia
35
+ @marginalia_components_were = Marginalia::Comment.components
36
+ Marginalia::Comment.components = []
37
+ end
38
+
39
+ def enable_marginalia
40
+ Marginalia::Comment.components = @marginalia_components_were
41
+ end
42
+ end
43
+ end
@@ -4,7 +4,7 @@ require 'safe-pg-migrations/base'
4
4
 
5
5
  module SafePgMigrations
6
6
  class Railtie < Rails::Railtie
7
- initializer 'sage_pg_migrations.insert_into_active_record' do
7
+ initializer 'safe_pg_migrations.insert_into_active_record' do
8
8
  ActiveSupport.on_load :active_record do
9
9
  ActiveRecord::Migration.prepend(SafePgMigrations::Migration)
10
10
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafePgMigrations
4
- VERSION = '0.0.2'
4
+ VERSION = '1.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: 0.0.2
4
+ version: 1.2.1
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: 2018-10-17 00:00:00.000000000 Z
12
+ date: 2021-01-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -166,6 +166,8 @@ files:
166
166
  - lib/safe-pg-migrations/plugins/idem_potent_statements.rb
167
167
  - lib/safe-pg-migrations/plugins/statement_insurer.rb
168
168
  - lib/safe-pg-migrations/plugins/statement_retrier.rb
169
+ - lib/safe-pg-migrations/plugins/useless_statements_logger.rb
170
+ - lib/safe-pg-migrations/plugins/verbose_sql_logger.rb
169
171
  - lib/safe-pg-migrations/railtie.rb
170
172
  - lib/safe-pg-migrations/version.rb
171
173
  homepage: https://github.com/doctolib/safe-pg-migrations
@@ -180,7 +182,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
180
182
  requirements:
181
183
  - - ">="
182
184
  - !ruby/object:Gem::Version
183
- version: '2.3'
185
+ version: '2.5'
184
186
  required_rubygems_version: !ruby/object:Gem::Requirement
185
187
  requirements:
186
188
  - - ">="
@@ -188,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
190
  version: '0'
189
191
  requirements: []
190
192
  rubyforge_project:
191
- rubygems_version: 2.6.14.1
193
+ rubygems_version: 2.7.3
192
194
  signing_key:
193
195
  specification_version: 4
194
196
  summary: Make your PG migrations safe.