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 +4 -4
- data/README.md +47 -11
- data/lib/safe-pg-migrations/base.rb +28 -35
- data/lib/safe-pg-migrations/configuration.rb +36 -2
- data/lib/safe-pg-migrations/helpers/blocking_activity_formatter.rb +23 -22
- data/lib/safe-pg-migrations/helpers/logger.rb +47 -0
- data/lib/safe-pg-migrations/helpers/session_setting_management.rb +35 -0
- data/lib/safe-pg-migrations/plugins/blocking_activity_logger.rb +25 -9
- data/lib/safe-pg-migrations/plugins/idempotent_statements.rb +43 -32
- data/lib/safe-pg-migrations/plugins/statement_insurer/add_column.rb +29 -16
- data/lib/safe-pg-migrations/plugins/statement_insurer/change_column_null.rb +46 -0
- data/lib/safe-pg-migrations/plugins/statement_insurer.rb +30 -63
- data/lib/safe-pg-migrations/plugins/statement_retrier.rb +2 -2
- data/lib/safe-pg-migrations/plugins/strong_migrations_integration.rb +75 -0
- data/lib/safe-pg-migrations/plugins/useless_statements_logger.rb +4 -2
- data/lib/safe-pg-migrations/railtie.rb +2 -0
- data/lib/safe-pg-migrations/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 624821b7134dee05186350edada6f27faaf8c820976837d32403dc5fffa4880a
|
4
|
+
data.tar.gz: bf41324b8d563de7bc893a302d45d8f175e890bb888a2f52e0b14be7676f942c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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 #
|
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
|
-
|
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
|
-
|
102
|
-
|
103
|
-
end
|
111
|
+
SafePgMigrations.with_current_migration(self) do
|
112
|
+
UselessStatementsLogger.warn_useless '`disable_ddl_transaction`' if super
|
104
113
|
|
105
|
-
|
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
|
8
|
-
|
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
|
-
|
8
|
+
Logger.say 'Could not find any blocking query.', sub_item: true
|
9
9
|
else
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
31
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
9
|
-
include
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
50
|
-
return super unless foreign_key_exists?(from_table,
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
111
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
12
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
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
|
-
!
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
40
|
-
|
41
|
-
end
|
37
|
+
|
38
|
+
super(from_table, to_table, **options)
|
42
39
|
|
43
40
|
return if validate_present
|
44
41
|
|
45
|
-
|
46
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
84
|
-
|
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
|
-
|
91
|
-
|
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
|
100
|
-
|
101
|
-
|
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
|
-
|
120
|
-
with_setting(:lock_timeout, 0, &block)
|
121
|
-
end
|
89
|
+
Helpers::Logger.say_method_call :drop_table, table_name, *args
|
122
90
|
|
123
|
-
|
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
|
-
|
28
|
+
Helpers::Logger.say "Retrying in #{retry_delay} seconds...", sub_item: true
|
29
29
|
sleep retry_delay
|
30
|
-
|
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
|
-
|
8
|
-
|
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
|
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
|
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-
|
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
|