safe-pg-migrations 1.0.0 → 1.1.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
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d806334c473708774d2180dbe91d2483b49f0d27b0a223a2973bc95d986d4030
|
4
|
+
data.tar.gz: 9b5c2d329d5cc1bd12c80944420e20eb5454e5b0a0f7fda2d6ec3fc3c3a5e042
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6786e68cce9dbd91a4633c80526a1fd63f6fbfa335d6e2b53d29b71ba0b566f9bb32169745bb13f89a059b82b0c0b21585a9bffdb9ebc751aa0e4a467f0f7934
|
7
|
+
data.tar.gz: 299a69f1d9413279fd61908a7d8f6eb65bd4c9912abcc1101ca8acbfee39c40d33086efa28a9f2133c0679c1ad9cdbbfe3ce84338f2175ad882b7d1ace707654
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
# 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.
|
7
|
+
- Ruby 2.5+
|
8
8
|
- Rails 5.2+
|
9
9
|
- PostgreSQL 9.3+
|
10
10
|
|
@@ -67,19 +67,21 @@ Active Record means developers don't have to be proficient in SQL to interact wi
|
|
67
67
|
|
68
68
|
## Feature set
|
69
69
|
|
70
|
-
|
70
|
+
<details><summary>Lock timeout</summary>
|
71
71
|
|
72
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
|
-
|
77
|
+
<details><summary>Statement timeout</summary>
|
77
78
|
|
78
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
|
-
|
84
|
+
<details><summary>Prevent wrapping migrations in transaction</summary>
|
83
85
|
|
84
86
|
When **Safe PG Migrations** is used, migrations are not wrapped in a transaction. This is for several reasons:
|
85
87
|
|
@@ -89,10 +91,22 @@ When **Safe PG Migrations** is used, migrations are not wrapped in a transaction
|
|
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
|
-
|
94
|
+
</details>
|
93
95
|
|
94
|
-
|
96
|
+
<details>
|
97
|
+
<summary>Safe <code>add_column</code></summary>
|
95
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**
|
96
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/).
|
97
111
|
|
98
112
|
**Safe PG Migrations** makes it safe by:
|
@@ -104,17 +118,11 @@ Adding a column with a default value and a not-null constraint is [dangerous](ht
|
|
104
118
|
|
105
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).
|
106
120
|
|
107
|
-
|
108
|
-
|
109
|
-
**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).
|
121
|
+
</blockquote>
|
110
122
|
|
111
|
-
|
112
|
-
```ruby
|
113
|
-
add_column :users, :created_at, default: 'clock_timestamp()'
|
114
|
-
```
|
115
|
-
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.
|
123
|
+
</details>
|
116
124
|
|
117
|
-
|
125
|
+
<details><summary>Safe <code>add_index</code> and <code>remove_index</code></summary>
|
118
126
|
|
119
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.
|
120
128
|
|
@@ -125,16 +133,30 @@ If you still get lock timeout while adding / removing indexes, it might be for o
|
|
125
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.
|
126
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/).
|
127
135
|
|
136
|
+
</details>
|
137
|
+
|
138
|
+
<details><summary>safe <code>add_foreign_key</code> (and <code>add_reference</code>)</summary>
|
128
139
|
|
129
|
-
|
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>
|
130
150
|
|
131
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>
|
132
153
|
|
133
|
-
|
154
|
+
<details><summary>Blocking activity logging</summary>
|
134
155
|
|
135
156
|
If a statement fails with a lock timeout, **Safe PG Migrations** will try to tell you what was the blocking statement.
|
157
|
+
</details>
|
136
158
|
|
137
|
-
|
159
|
+
<details><summary>Verbose SQL logging</summary>
|
138
160
|
|
139
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:
|
140
162
|
```bash
|
@@ -151,7 +173,7 @@ add_index :users, :age
|
|
151
173
|
-> 0.0175s
|
152
174
|
== 20191215132355 SampleIndex: migrated (0.0200s) =============================
|
153
175
|
```
|
154
|
-
**
|
176
|
+
**Safe PG Migrations** will output the following logs:
|
155
177
|
```ruby
|
156
178
|
add_index :users, :age
|
157
179
|
|
@@ -175,6 +197,8 @@ So you can actually check that the `CREATE INDEX` statement will be performed co
|
|
175
197
|
|
176
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*
|
177
199
|
|
200
|
+
</details>
|
201
|
+
|
178
202
|
## Configuration
|
179
203
|
|
180
204
|
**Safe PG Migrations** can be customized, here is an example of a Rails initializer (the values are the default ones):
|
@@ -12,6 +12,12 @@ 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
|
+
|
15
21
|
def remove_column(table_name, column_name, type = nil, options = {})
|
16
22
|
return super if column_exists?(table_name, column_name)
|
17
23
|
|
@@ -26,6 +32,25 @@ module SafePgMigrations
|
|
26
32
|
SafePgMigrations.say("/!\\ Index '#{index_name}' not found on table '#{table_name}'. Skipping statement.", true)
|
27
33
|
end
|
28
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 unless table_exists?(table_name)
|
47
|
+
|
48
|
+
SafePgMigrations.say(
|
49
|
+
"/!\\ Table '#{table_name}' already exists. Skipping statement.",
|
50
|
+
true
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
29
54
|
private
|
30
55
|
|
31
56
|
def index_valid?(index_name)
|
@@ -4,20 +4,20 @@ module SafePgMigrations
|
|
4
4
|
module StatementInsurer
|
5
5
|
PG_11_VERSION_NUM = 110_000
|
6
6
|
|
7
|
-
%i[change_column_null
|
7
|
+
%i[change_column_null change_column create_table].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
11
|
end
|
12
12
|
|
13
|
-
def add_column(table_name, column_name, type, **options)
|
13
|
+
def add_column(table_name, column_name, type, **options) # rubocop:disable Metrics/CyclomaticComplexity
|
14
14
|
need_default_value_backfill = SafePgMigrations.pg_version_num < PG_11_VERSION_NUM
|
15
15
|
|
16
16
|
default = options.delete(:default) if need_default_value_backfill
|
17
17
|
null = options.delete(:null)
|
18
18
|
|
19
19
|
if !default.nil? || null == false
|
20
|
-
SafePgMigrations.say_method_call(:add_column, table_name, column_name, type,
|
20
|
+
SafePgMigrations.say_method_call(:add_column, table_name, column_name, type, options)
|
21
21
|
end
|
22
22
|
|
23
23
|
super
|
@@ -36,9 +36,19 @@ module SafePgMigrations
|
|
36
36
|
end
|
37
37
|
end
|
38
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
|
+
|
39
49
|
def add_index(table_name, column_name, **options)
|
40
50
|
options[:algorithm] = :concurrently
|
41
|
-
SafePgMigrations.say_method_call(:add_index, table_name, column_name,
|
51
|
+
SafePgMigrations.say_method_call(:add_index, table_name, column_name, options)
|
42
52
|
|
43
53
|
with_index_timeouts { super }
|
44
54
|
end
|
@@ -46,7 +56,7 @@ module SafePgMigrations
|
|
46
56
|
def remove_index(table_name, options = {})
|
47
57
|
options = { column: options } unless options.is_a?(Hash)
|
48
58
|
options[:algorithm] = :concurrently
|
49
|
-
SafePgMigrations.say_method_call(:remove_index, table_name,
|
59
|
+
SafePgMigrations.say_method_call(:remove_index, table_name, options)
|
50
60
|
|
51
61
|
with_index_timeouts { super }
|
52
62
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safe-pg-migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthieu Prat
|
8
8
|
- Romain Choquet
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-11-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -173,7 +173,7 @@ homepage: https://github.com/doctolib/safe-pg-migrations
|
|
173
173
|
licenses:
|
174
174
|
- MIT
|
175
175
|
metadata: {}
|
176
|
-
post_install_message:
|
176
|
+
post_install_message:
|
177
177
|
rdoc_options: []
|
178
178
|
require_paths:
|
179
179
|
- lib
|
@@ -181,16 +181,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
181
181
|
requirements:
|
182
182
|
- - ">="
|
183
183
|
- !ruby/object:Gem::Version
|
184
|
-
version: '2.
|
184
|
+
version: '2.5'
|
185
185
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
186
|
requirements:
|
187
187
|
- - ">="
|
188
188
|
- !ruby/object:Gem::Version
|
189
189
|
version: '0'
|
190
190
|
requirements: []
|
191
|
-
rubyforge_project:
|
192
|
-
rubygems_version: 2.
|
193
|
-
signing_key:
|
191
|
+
rubyforge_project:
|
192
|
+
rubygems_version: 2.7.3
|
193
|
+
signing_key:
|
194
194
|
specification_version: 4
|
195
195
|
summary: Make your PG migrations safe.
|
196
196
|
test_files: []
|