safe-pg-migrations 1.3.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fdee6e752da3fbdec93e5fd5c72fb370497ddd66f1cc27f33609c2a31e364c53
4
- data.tar.gz: 143a5dcbf614abed765fac78b16969d900ded5ec574f11d939d802b94d7e2586
3
+ metadata.gz: c5137a033537f088b0a0c3f41dc8b632d5c37ef81cf5ac25bc62d6b01e62c56c
4
+ data.tar.gz: 5f0029750d93bcb04ffcb65e8a8bf0b6a8420ee1f6d7c12a85efac967e3b5c93
5
5
  SHA512:
6
- metadata.gz: 41c264aa05c7b8ad5fe90f7ecdc88fa700504562e69695d5fd9f4dc9ac63315fb666818554e1387251b367aed241fccff31154589f70309905e1ed1cfae90c68
7
- data.tar.gz: 9386e6a8b0366387543998ec5cec42e719d2a4138d5eb52f13f6580d2a3d31e48bd438760b358beb1ab5eb32bd8598927d69bafd03756108a514e88eeb431022
6
+ metadata.gz: afbb64bcc3de2680200ec2cc735b6fa7fc7bb02a71b056be7dcafd3a02a9a11895398632e128ad727e6b089e149ccf75617c5f82674bd1472b16af79c15c0f77
7
+ data.tar.gz: '0946ab447b6c503ff9965f71edba84e973ddc6e5bc658fe9785672d23c3dc6b0ace071e9081727defd8585dba3d6faedb22145bd9fe8c84e0cd735e4ce44457f'
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ActiveRecord migrations for Postgres made safe.
4
4
 
5
+ ![safe-pg-migrations](./logo.png)
6
+
5
7
  ## Requirements
6
8
 
7
9
  - Ruby 2.5+
@@ -91,7 +93,7 @@ When **Safe PG Migrations** is used, migrations are not wrapped in a transaction
91
93
  - In order to be able to retry statements that have failed because of a lock timeout, we have to be outside a transaction.
92
94
  - In order to add an index concurrently, we have to be outside a transaction.
93
95
 
94
- 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.
96
+ Note that if a migration fails, it won't be rolled back. This can result in migrations being partially applied. In that case, they need to be manually reverted.
95
97
 
96
98
  </details>
97
99
 
@@ -108,7 +110,7 @@ PG will still needs to update every row of the table, and will most likely state
108
110
 
109
111
  <blockquote>
110
112
 
111
- **Note: Pre-postgre 11**
113
+ **Note: Pre-postgres 11**
112
114
  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/).
113
115
 
114
116
  **Safe PG Migrations** makes it safe by:
@@ -120,7 +122,7 @@ Adding a column with a default value and a not-null constraint is [dangerous](ht
120
122
 
121
123
  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).
122
124
 
123
- </blockquote>
125
+ </blockquote>
124
126
 
125
127
  </details>
126
128
 
@@ -137,7 +139,7 @@ If you still get lock timeout while adding / removing indexes, it might be for o
137
139
 
138
140
  </details>
139
141
 
140
- <details><summary id="safe_add_foreign_key">safe <code>add_foreign_key</code> (and <code>add_reference</code>)</summary>
142
+ <details><summary id="safe_add_foreign_key">Safe <code>add_foreign_key</code> (and <code>add_reference</code>)</summary>
141
143
 
142
144
  Adding a foreign key requires a `SHARE ROW EXCLUSIVE` lock, which **prevent writing in the tables** while the migration is running.
143
145
 
@@ -150,7 +152,7 @@ Adding the constraint itself is rather fast, the major part of the time is spent
150
152
 
151
153
  <details><summary>Retry after lock timeout</summary>
152
154
 
153
- 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)
155
+ When a statement fails with a lock timeout, **Safe PG Migrations** retries it (5 times max) [list of retriable statements](https://github.com/doctolib/safe-pg-migrations/blob/66933256252b6bbf12e404b829a361dbba30e684/lib/safe-pg-migrations/plugins/statement_retrier.rb#L5)
154
156
  </details>
155
157
 
156
158
  <details><summary>Blocking activity logging</summary>
@@ -219,11 +221,11 @@ SafePgMigrations.config.retry_delay = 1.minute # Delay between retries for retry
219
221
  SafePgMigrations.config.max_tries = 5 # Number of retries before abortion of the migration
220
222
  ```
221
223
 
222
- ## Runnings tests
224
+ ## Running tests
223
225
 
224
226
  ```bash
225
227
  bundle
226
- psql -h localhost -U postgres -c 'CREATE DATABASE safe_pg_migrations_test'
228
+ psql -h localhost -c 'CREATE DATABASE safe_pg_migrations_test'
227
229
  rake test
228
230
  ```
229
231
 
@@ -257,3 +259,4 @@ Interesting reads:
257
259
  - [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/)
258
260
  - [Rails Migrations with Zero Downtime](https://blog.codeship.com/rails-migrations-zero-downtime/)
259
261
  - [Stop worrying about PostgreSQL locks in your Rails migrations](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9)
262
+ - [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)
@@ -6,17 +6,19 @@ require 'safe-pg-migrations/plugins/verbose_sql_logger'
6
6
  require 'safe-pg-migrations/plugins/blocking_activity_logger'
7
7
  require 'safe-pg-migrations/plugins/statement_insurer'
8
8
  require 'safe-pg-migrations/plugins/statement_retrier'
9
- require 'safe-pg-migrations/plugins/idem_potent_statements'
9
+ require 'safe-pg-migrations/plugins/idempotent_statements'
10
10
  require 'safe-pg-migrations/plugins/useless_statements_logger'
11
+ require 'safe-pg-migrations/plugins/legacy_active_record_support'
11
12
 
12
13
  module SafePgMigrations
13
14
  # Order matters: the bottom-most plugin will have precedence
14
15
  PLUGINS = [
15
16
  BlockingActivityLogger,
16
- IdemPotentStatements,
17
+ IdempotentStatements,
17
18
  StatementRetrier,
18
19
  StatementInsurer,
19
20
  UselessStatementsLogger,
21
+ LegacyActiveRecordSupport,
20
22
  ].freeze
21
23
 
22
24
  class << self
@@ -85,7 +87,16 @@ module SafePgMigrations
85
87
  true
86
88
  end
87
89
 
88
- SAFE_METHODS = %i[execute add_column add_index add_reference add_belongs_to change_column_null].freeze
90
+ SAFE_METHODS = %i[
91
+ execute
92
+ add_column
93
+ add_index
94
+ add_reference
95
+ add_belongs_to
96
+ change_column_null
97
+ add_foreign_key
98
+ ].freeze
99
+
89
100
  SAFE_METHODS.each do |method|
90
101
  define_method method do |*args|
91
102
  return super(*args) unless respond_to?(:safety_assured)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafePgMigrations
4
- module BlockingActivityLogger
4
+ module BlockingActivityLogger # rubocop:disable Metrics/ModuleLength
5
5
  FILTERED_COLUMNS = %w[
6
6
  blocked_activity.xact_start
7
7
  blocked_locks.locktype
@@ -113,6 +113,7 @@ module SafePgMigrations
113
113
  end
114
114
 
115
115
  def format_start_time(start_time, reference_time = Time.now)
116
+ start_time = Time.parse(start_time) unless start_time.is_a? Time
116
117
  duration = (reference_time - start_time).round
117
118
  "transaction started #{duration} #{'second'.pluralize(duration)} ago"
118
119
  end
@@ -1,15 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafePgMigrations
4
- module IdemPotentStatements
4
+ module IdempotentStatements
5
5
  ruby2_keywords def add_index(table_name, column_name, *args)
6
6
  options = args.last.is_a?(Hash) ? args.last : {}
7
- index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, index_column_names(column_name))
8
- return super unless index_name_exists?(table_name, index_name)
9
7
 
10
- return if index_valid?(index_name)
8
+ index_definition = index_definition(table_name, column_name, **options)
11
9
 
12
- remove_index(table_name, name: index_name)
10
+ return super unless index_name_exists?(index_definition.table, index_definition.name)
11
+
12
+ if index_valid?(index_definition.name)
13
+ SafePgMigrations.say(
14
+ "/!\\ Index '#{index_definition.name}' already exists in '#{table_name}'. Skipping statement.",
15
+ true
16
+ )
17
+ return
18
+ end
19
+
20
+ remove_index(table_name, name: index_definition.name)
13
21
  super
14
22
  end
15
23
 
@@ -62,6 +70,13 @@ module SafePgMigrations
62
70
  end
63
71
  end
64
72
 
73
+ protected
74
+
75
+ def index_definition(table_name, column_name, **options)
76
+ index_definition, = add_index_options(table_name, column_name, **options)
77
+ index_definition
78
+ end
79
+
65
80
  private
66
81
 
67
82
  def index_valid?(index_name)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module LegacyActiveRecordSupport
5
+ ruby2_keywords def validate_foreign_key(*args)
6
+ return super(*args) if satisfied? '>=6.0.0'
7
+
8
+ from_table, to_table, options = args
9
+ super(from_table, to_table || options)
10
+ end
11
+
12
+ ruby2_keywords def foreign_key_exists?(*args)
13
+ return super(*args) if satisfied? '>=6.0.0'
14
+
15
+ from_table, to_table, options = args
16
+ super(from_table, to_table || options)
17
+ end
18
+
19
+ protected
20
+
21
+ IndexDefinition = Struct.new(:table, :name)
22
+
23
+ def index_definition(table_name, column_name, **options)
24
+ return super(table_name, column_name, **options) if satisfied? '>=6.1.0'
25
+
26
+ index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, index_column_names(column_name))
27
+ validate_index_length!(table_name, index_name, options.fetch(:internal, false))
28
+
29
+ IndexDefinition.new(table_name, index_name)
30
+ end
31
+
32
+ private
33
+
34
+ def satisfied?(version)
35
+ Gem::Requirement.new(version).satisfied_by? Gem::Version.new(::ActiveRecord::VERSION::STRING)
36
+ end
37
+ end
38
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafePgMigrations
4
- module StatementInsurer # rubocop:disable Metrics/ModuleLength
4
+ module StatementInsurer
5
5
  PG_11_VERSION_NUM = 110_000
6
6
 
7
7
  %i[change_column_null change_column].each do |method|
@@ -85,20 +85,11 @@ module SafePgMigrations
85
85
  end
86
86
 
87
87
  def backfill_column_default(table_name, column_name)
88
- quoted_table_name = quote_table_name(table_name)
88
+ model = Class.new(ActiveRecord::Base) { self.table_name = table_name }
89
89
  quoted_column_name = quote_column_name(column_name)
90
- primary_key_offset = 0
91
- loop do
92
- ids = query_values <<~SQL.squish
93
- SELECT id FROM #{quoted_table_name} WHERE id > #{primary_key_offset}
94
- ORDER BY id LIMIT #{SafePgMigrations.config.batch_size}
95
- SQL
96
- break if ids.empty?
97
-
98
- primary_key_offset = ids.last
99
- execute <<~SQL.squish
100
- UPDATE #{quoted_table_name} SET #{quoted_column_name} = DEFAULT WHERE id IN (#{ids.join(',')})
101
- SQL
90
+
91
+ model.in_batches(of: SafePgMigrations.config.batch_size).each do |relation|
92
+ relation.update_all("#{quoted_column_name} = DEFAULT")
102
93
  end
103
94
  end
104
95
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafePgMigrations
4
- VERSION = '1.3.0'
4
+ VERSION = '1.4.2'
5
5
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe-pg-migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu Prat
8
8
  - Romain Choquet
9
+ - Thomas Hareau
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2021-06-28 00:00:00.000000000 Z
13
+ date: 2022-03-10 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: activerecord
@@ -53,120 +54,8 @@ dependencies:
53
54
  - - ">="
54
55
  - !ruby/object:Gem::Version
55
56
  version: 0.0.4
56
- - !ruby/object:Gem::Dependency
57
- name: bundler
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: '0'
63
- type: :development
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: '0'
70
- - !ruby/object:Gem::Dependency
71
- name: minitest
72
- requirement: !ruby/object:Gem::Requirement
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- version: '0'
77
- type: :development
78
- prerelease: false
79
- version_requirements: !ruby/object:Gem::Requirement
80
- requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- version: '0'
84
- - !ruby/object:Gem::Dependency
85
- name: mocha
86
- requirement: !ruby/object:Gem::Requirement
87
- requirements:
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- version: '0'
91
- type: :development
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - ">="
96
- - !ruby/object:Gem::Version
97
- version: '0'
98
- - !ruby/object:Gem::Dependency
99
- name: pg
100
- requirement: !ruby/object:Gem::Requirement
101
- requirements:
102
- - - ">="
103
- - !ruby/object:Gem::Version
104
- version: '0'
105
- type: :development
106
- prerelease: false
107
- version_requirements: !ruby/object:Gem::Requirement
108
- requirements:
109
- - - ">="
110
- - !ruby/object:Gem::Version
111
- version: '0'
112
- - !ruby/object:Gem::Dependency
113
- name: pry
114
- requirement: !ruby/object:Gem::Requirement
115
- requirements:
116
- - - ">="
117
- - !ruby/object:Gem::Version
118
- version: '0'
119
- type: :development
120
- prerelease: false
121
- version_requirements: !ruby/object:Gem::Requirement
122
- requirements:
123
- - - ">="
124
- - !ruby/object:Gem::Version
125
- version: '0'
126
- - !ruby/object:Gem::Dependency
127
- name: pry-coolline
128
- requirement: !ruby/object:Gem::Requirement
129
- requirements:
130
- - - ">="
131
- - !ruby/object:Gem::Version
132
- version: '0'
133
- type: :development
134
- prerelease: false
135
- version_requirements: !ruby/object:Gem::Requirement
136
- requirements:
137
- - - ">="
138
- - !ruby/object:Gem::Version
139
- version: '0'
140
- - !ruby/object:Gem::Dependency
141
- name: rake
142
- requirement: !ruby/object:Gem::Requirement
143
- requirements:
144
- - - ">="
145
- - !ruby/object:Gem::Version
146
- version: '0'
147
- type: :development
148
- prerelease: false
149
- version_requirements: !ruby/object:Gem::Requirement
150
- requirements:
151
- - - ">="
152
- - !ruby/object:Gem::Version
153
- version: '0'
154
- - !ruby/object:Gem::Dependency
155
- name: rubocop
156
- requirement: !ruby/object:Gem::Requirement
157
- requirements:
158
- - - ">="
159
- - !ruby/object:Gem::Version
160
- version: '0'
161
- type: :development
162
- prerelease: false
163
- version_requirements: !ruby/object:Gem::Requirement
164
- requirements:
165
- - - ">="
166
- - !ruby/object:Gem::Version
167
- version: '0'
168
57
  description: Make your PG migrations safe.
169
- email: matthieuprat@gmail.com
58
+ email:
170
59
  executables: []
171
60
  extensions: []
172
61
  extra_rdoc_files: []
@@ -177,7 +66,8 @@ files:
177
66
  - lib/safe-pg-migrations/base.rb
178
67
  - lib/safe-pg-migrations/configuration.rb
179
68
  - lib/safe-pg-migrations/plugins/blocking_activity_logger.rb
180
- - lib/safe-pg-migrations/plugins/idem_potent_statements.rb
69
+ - lib/safe-pg-migrations/plugins/idempotent_statements.rb
70
+ - lib/safe-pg-migrations/plugins/legacy_active_record_support.rb
181
71
  - lib/safe-pg-migrations/plugins/statement_insurer.rb
182
72
  - lib/safe-pg-migrations/plugins/statement_retrier.rb
183
73
  - lib/safe-pg-migrations/plugins/useless_statements_logger.rb
@@ -187,7 +77,12 @@ files:
187
77
  homepage: https://github.com/doctolib/safe-pg-migrations
188
78
  licenses:
189
79
  - MIT
190
- metadata: {}
80
+ metadata:
81
+ bug_tracker_uri: https://github.com/doctolib/safe-pg-migrations/issues
82
+ homepage_uri: https://github.com/doctolib/safe-pg-migrations#safe-pg-migrations
83
+ mailing_list_uri: https://doctolib.engineering/engineering-news-ruby-rails-react
84
+ source_code_uri: https://github.com/doctolib/safe-pg-migrations
85
+ contributors_uri: https://github.com/doctolib/safe-pg-migrations/graphs/contributors
191
86
  post_install_message:
192
87
  rdoc_options: []
193
88
  require_paths: