safe-pg-migrations 2.1.0 → 2.2.1
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 +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
|