strong_migrations 0.6.5 → 0.7.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/CHANGELOG.md +28 -0
- data/LICENSE.txt +1 -1
- data/README.md +189 -138
- data/lib/generators/strong_migrations/install_generator.rb +28 -0
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +22 -0
- data/lib/strong_migrations.rb +43 -50
- data/lib/strong_migrations/checker.rb +139 -29
- data/lib/strong_migrations/railtie.rb +0 -4
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/tasks/strong_migrations.rake +0 -6
- metadata +4 -3
- data/lib/strong_migrations/migration_helpers.rb +0 -117
@@ -0,0 +1,28 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module StrongMigrations
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.join(__dir__, "templates")
|
7
|
+
|
8
|
+
def create_initializer
|
9
|
+
template "initializer.rb", "config/initializers/strong_migrations.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
def start_after
|
13
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
14
|
+
end
|
15
|
+
|
16
|
+
def target_version
|
17
|
+
case ActiveRecord::Base.connection_config[:adapter].to_s
|
18
|
+
when /mysql/
|
19
|
+
# could try to connect to database and check for MariaDB
|
20
|
+
# but this should be fine
|
21
|
+
'"8.0.12"'
|
22
|
+
else
|
23
|
+
"10"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Mark existing migrations as safe
|
2
|
+
StrongMigrations.start_after = <%= start_after %>
|
3
|
+
|
4
|
+
# Set timeouts for migrations
|
5
|
+
# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user
|
6
|
+
StrongMigrations.lock_timeout = 10.seconds
|
7
|
+
StrongMigrations.statement_timeout = 1.hour
|
8
|
+
|
9
|
+
# Analyze tables after indexes are added
|
10
|
+
# Outdated statistics can sometimes hurt performance
|
11
|
+
StrongMigrations.auto_analyze = true
|
12
|
+
|
13
|
+
# Set the version of the production database
|
14
|
+
# so the right checks are run in development
|
15
|
+
# StrongMigrations.target_version = <%= target_version %>
|
16
|
+
|
17
|
+
# Add custom checks
|
18
|
+
# StrongMigrations.add_check do |method, args|
|
19
|
+
# if method == :add_index && args[0].to_s == "users"
|
20
|
+
# stop! "No more indexes on the users table"
|
21
|
+
# end
|
22
|
+
# end
|
data/lib/strong_migrations.rb
CHANGED
@@ -5,7 +5,6 @@ require "active_support"
|
|
5
5
|
require "strong_migrations/checker"
|
6
6
|
require "strong_migrations/database_tasks"
|
7
7
|
require "strong_migrations/migration"
|
8
|
-
require "strong_migrations/migration_helpers"
|
9
8
|
require "strong_migrations/version"
|
10
9
|
|
11
10
|
# integrations
|
@@ -18,14 +17,15 @@ module StrongMigrations
|
|
18
17
|
class << self
|
19
18
|
attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
|
20
19
|
:target_postgresql_version, :target_mysql_version, :target_mariadb_version,
|
21
|
-
:enabled_checks, :lock_timeout, :statement_timeout, :
|
20
|
+
:enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version
|
21
|
+
attr_writer :lock_timeout_limit
|
22
22
|
end
|
23
23
|
self.auto_analyze = false
|
24
24
|
self.start_after = 0
|
25
25
|
self.checks = []
|
26
26
|
self.error_messages = {
|
27
27
|
add_column_default:
|
28
|
-
"Adding a column with a non-null default
|
28
|
+
"Adding a column with a non-null default blocks %{rewrite_blocks} while the entire table is rewritten.
|
29
29
|
Instead, add the column without a default value, then change the default.
|
30
30
|
|
31
31
|
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
@@ -50,12 +50,18 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
50
50
|
end",
|
51
51
|
|
52
52
|
add_column_json:
|
53
|
-
"There's no equality operator for the json column type, which can
|
54
|
-
|
53
|
+
"There's no equality operator for the json column type, which can cause errors for
|
54
|
+
existing SELECT DISTINCT queries in your application. Use jsonb instead.
|
55
|
+
|
56
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
57
|
+
def change
|
58
|
+
%{command}
|
59
|
+
end
|
60
|
+
end",
|
55
61
|
|
56
62
|
change_column:
|
57
|
-
"Changing the type of an existing column
|
58
|
-
|
63
|
+
"Changing the type of an existing column blocks %{rewrite_blocks}
|
64
|
+
while the entire table is rewritten. A safer approach is to:
|
59
65
|
|
60
66
|
1. Create a new column
|
61
67
|
2. Write to both columns
|
@@ -64,7 +70,10 @@ table and indexes to be rewritten. A safer approach is to:
|
|
64
70
|
5. Stop writing to the old column
|
65
71
|
6. Drop the old column",
|
66
72
|
|
67
|
-
|
73
|
+
change_column_with_not_null:
|
74
|
+
"Changing the type is safe, but setting NOT NULL is not.",
|
75
|
+
|
76
|
+
remove_column: "Active Record caches attributes, which causes problems
|
68
77
|
when removing columns. Be sure to ignore the column%{column_suffix}:
|
69
78
|
|
70
79
|
class %{model} < %{base_model}
|
@@ -80,7 +89,8 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
80
89
|
end",
|
81
90
|
|
82
91
|
rename_column:
|
83
|
-
"Renaming a column
|
92
|
+
"Renaming a column that's in use will cause errors
|
93
|
+
in your application. A safer approach is to:
|
84
94
|
|
85
95
|
1. Create a new column
|
86
96
|
2. Write to both columns
|
@@ -90,7 +100,8 @@ end",
|
|
90
100
|
6. Drop the old column",
|
91
101
|
|
92
102
|
rename_table:
|
93
|
-
"Renaming a table
|
103
|
+
"Renaming a table that's in use will cause errors
|
104
|
+
in your application. A safer approach is to:
|
94
105
|
|
95
106
|
1. Create a new table. Don't forget to recreate indexes from the old table
|
96
107
|
2. Write to both tables
|
@@ -111,7 +122,7 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
111
122
|
end",
|
112
123
|
|
113
124
|
add_index:
|
114
|
-
"Adding an index non-concurrently
|
125
|
+
"Adding an index non-concurrently blocks writes. Instead, use:
|
115
126
|
|
116
127
|
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
117
128
|
disable_ddl_transaction!
|
@@ -122,7 +133,7 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
122
133
|
end",
|
123
134
|
|
124
135
|
remove_index:
|
125
|
-
"Removing an index non-concurrently
|
136
|
+
"Removing an index non-concurrently blocks writes. Instead, use:
|
126
137
|
|
127
138
|
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
128
139
|
disable_ddl_transaction!
|
@@ -165,9 +176,8 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
165
176
|
end",
|
166
177
|
|
167
178
|
change_column_null_postgresql:
|
168
|
-
"Setting NOT NULL on
|
169
|
-
|
170
|
-
validate it in a separate migration with a more agreeable RowShareLock.
|
179
|
+
"Setting NOT NULL on an existing column blocks reads and writes while every row is checked.
|
180
|
+
Instead, add a check constraint and validate it in a separate migration.
|
171
181
|
|
172
182
|
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
173
183
|
def change
|
@@ -181,26 +191,13 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
181
191
|
end
|
182
192
|
end",
|
183
193
|
|
184
|
-
change_column_null_postgresql_helper:
|
185
|
-
"Setting NOT NULL on a column requires an AccessExclusiveLock,
|
186
|
-
which is expensive on large tables. Instead, we can use a constraint and
|
187
|
-
validate it in a separate step with a more agreeable RowShareLock.
|
188
|
-
|
189
|
-
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
190
|
-
disable_ddl_transaction!
|
191
|
-
|
192
|
-
def change
|
193
|
-
%{command}
|
194
|
-
end
|
195
|
-
end",
|
196
|
-
|
197
194
|
change_column_null_mysql:
|
198
195
|
"Setting NOT NULL on an existing column is not safe with your database engine.",
|
199
196
|
|
200
197
|
add_foreign_key:
|
201
|
-
"
|
202
|
-
|
203
|
-
|
198
|
+
"Adding a foreign key blocks writes on both tables. Instead,
|
199
|
+
add the foreign key without validating existing rows,
|
200
|
+
then validate them in a separate migration.
|
204
201
|
|
205
202
|
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
206
203
|
def change
|
@@ -214,21 +211,24 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
214
211
|
end
|
215
212
|
end",
|
216
213
|
|
217
|
-
|
218
|
-
"
|
219
|
-
|
220
|
-
|
214
|
+
validate_foreign_key:
|
215
|
+
"Validating a foreign key while writes are blocked is dangerous.
|
216
|
+
Use disable_ddl_transaction! or a separate migration."
|
217
|
+
}
|
218
|
+
self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
|
219
|
+
self.check_down = false
|
221
220
|
|
222
|
-
|
223
|
-
|
221
|
+
# private
|
222
|
+
def self.developer_env?
|
223
|
+
defined?(Rails) && (Rails.env.development? || Rails.env.test?)
|
224
|
+
end
|
224
225
|
|
225
|
-
def
|
226
|
-
|
226
|
+
def self.lock_timeout_limit
|
227
|
+
unless defined?(@lock_timeout_limit)
|
228
|
+
@lock_timeout_limit = developer_env? ? false : 10
|
229
|
+
end
|
230
|
+
@lock_timeout_limit
|
227
231
|
end
|
228
|
-
end",
|
229
|
-
}
|
230
|
-
self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
|
231
|
-
self.helpers = false
|
232
232
|
|
233
233
|
def self.add_check(&block)
|
234
234
|
checks << block
|
@@ -250,13 +250,6 @@ end",
|
|
250
250
|
false
|
251
251
|
end
|
252
252
|
end
|
253
|
-
|
254
|
-
# def self.enable_helpers
|
255
|
-
# unless helpers
|
256
|
-
# ActiveRecord::Migration.include(StrongMigrations::MigrationHelpers)
|
257
|
-
# self.helpers = true
|
258
|
-
# end
|
259
|
-
# end
|
260
253
|
end
|
261
254
|
|
262
255
|
ActiveSupport.on_load(:active_record) do
|
@@ -7,6 +7,7 @@ module StrongMigrations
|
|
7
7
|
@new_tables = []
|
8
8
|
@safe = false
|
9
9
|
@timeouts_set = false
|
10
|
+
@lock_timeout_checked = false
|
10
11
|
end
|
11
12
|
|
12
13
|
def safety_assured
|
@@ -21,6 +22,7 @@ module StrongMigrations
|
|
21
22
|
|
22
23
|
def perform(method, *args)
|
23
24
|
set_timeouts
|
25
|
+
check_lock_timeout
|
24
26
|
|
25
27
|
unless safe?
|
26
28
|
case method
|
@@ -94,11 +96,13 @@ Then add the NOT NULL constraint in separate migrations."
|
|
94
96
|
change_command: command_str("change_column_default", [table, column, default]),
|
95
97
|
remove_command: command_str("remove_column", [table, column]),
|
96
98
|
code: backfill_code(table, column, default),
|
97
|
-
append: append
|
99
|
+
append: append,
|
100
|
+
rewrite_blocks: rewrite_blocks
|
98
101
|
end
|
99
102
|
|
100
103
|
if type.to_s == "json" && postgresql?
|
101
|
-
raise_error :add_column_json
|
104
|
+
raise_error :add_column_json,
|
105
|
+
command: command_str("add_column", [table, column, :jsonb, options])
|
102
106
|
end
|
103
107
|
when :change_column
|
104
108
|
table, column, type, options = args
|
@@ -107,15 +111,24 @@ Then add the NOT NULL constraint in separate migrations."
|
|
107
111
|
safe = false
|
108
112
|
existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
|
109
113
|
if existing_column
|
110
|
-
|
114
|
+
existing_type = existing_column.sql_type.split("(").first
|
111
115
|
if postgresql?
|
112
116
|
case type.to_s
|
113
|
-
when "string"
|
114
|
-
# safe to
|
115
|
-
safe
|
117
|
+
when "string"
|
118
|
+
# safe to increase limit or remove it
|
119
|
+
# not safe to decrease limit or add a limit
|
120
|
+
case existing_type
|
121
|
+
when "character varying"
|
122
|
+
safe = !options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
|
123
|
+
when "text"
|
124
|
+
safe = !options[:limit]
|
125
|
+
end
|
126
|
+
when "text"
|
127
|
+
# safe to change varchar to text (and text to text)
|
128
|
+
safe = ["character varying", "text"].include?(existing_type)
|
116
129
|
when "numeric", "decimal"
|
117
130
|
# numeric and decimal are equivalent and can be used interchangably
|
118
|
-
safe = ["numeric", "decimal"].include?(
|
131
|
+
safe = ["numeric", "decimal"].include?(existing_type) &&
|
119
132
|
(
|
120
133
|
(
|
121
134
|
# unconstrained
|
@@ -128,7 +141,7 @@ Then add the NOT NULL constraint in separate migrations."
|
|
128
141
|
)
|
129
142
|
)
|
130
143
|
when "datetime", "timestamp", "timestamptz"
|
131
|
-
safe = ["timestamp without time zone", "timestamp with time zone"].include?(
|
144
|
+
safe = ["timestamp without time zone", "timestamp with time zone"].include?(existing_type) &&
|
132
145
|
postgresql_version >= Gem::Version.new("12") &&
|
133
146
|
connection.select_all("SHOW timezone").first["TimeZone"] == "UTC"
|
134
147
|
end
|
@@ -140,13 +153,19 @@ Then add the NOT NULL constraint in separate migrations."
|
|
140
153
|
# increased limit, but doesn't change number of length bytes
|
141
154
|
# 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
|
142
155
|
limit = options[:limit] || 255
|
143
|
-
safe = ["varchar"].include?(
|
156
|
+
safe = ["varchar"].include?(existing_type) &&
|
144
157
|
limit >= existing_column.limit &&
|
145
158
|
(limit <= 255 || existing_column.limit > 255)
|
146
159
|
end
|
147
160
|
end
|
148
161
|
end
|
149
|
-
|
162
|
+
|
163
|
+
# unsafe to set NOT NULL for safe types
|
164
|
+
if safe && existing_column.null && options[:null] == false
|
165
|
+
raise_error :change_column_with_not_null
|
166
|
+
end
|
167
|
+
|
168
|
+
raise_error :change_column, rewrite_blocks: rewrite_blocks unless safe
|
150
169
|
when :create_table
|
151
170
|
table, options = args
|
152
171
|
options ||= {}
|
@@ -174,7 +193,7 @@ Then add the NOT NULL constraint in separate migrations."
|
|
174
193
|
end
|
175
194
|
|
176
195
|
if options.delete(:foreign_key)
|
177
|
-
headline = "Adding a
|
196
|
+
headline = "Adding a foreign key blocks writes on both tables."
|
178
197
|
append = "
|
179
198
|
|
180
199
|
Then add the foreign key in separate migrations."
|
@@ -194,16 +213,25 @@ Then add the foreign key in separate migrations."
|
|
194
213
|
table, column, null, default = args
|
195
214
|
if !null
|
196
215
|
if postgresql?
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
216
|
+
safe = false
|
217
|
+
if postgresql_version >= Gem::Version.new("12")
|
218
|
+
# TODO likely need to quote the column in some situations
|
219
|
+
safe = constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" }
|
220
|
+
end
|
221
|
+
|
222
|
+
unless safe
|
201
223
|
# match https://github.com/nullobject/rein
|
202
224
|
constraint_name = "#{table}_#{column}_null"
|
203
225
|
|
226
|
+
validate_constraint_code = String.new(constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name]))
|
227
|
+
if postgresql_version >= Gem::Version.new("12")
|
228
|
+
validate_constraint_code << "\n #{command_str(:change_column_null, [table, column, null])}"
|
229
|
+
validate_constraint_code << "\n #{constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])}"
|
230
|
+
end
|
231
|
+
|
204
232
|
raise_error :change_column_null_postgresql,
|
205
233
|
add_constraint_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
|
206
|
-
validate_constraint_code:
|
234
|
+
validate_constraint_code: validate_constraint_code
|
207
235
|
end
|
208
236
|
elsif mysql? || mariadb?
|
209
237
|
raise_error :change_column_null_mysql
|
@@ -220,10 +248,7 @@ Then add the foreign key in separate migrations."
|
|
220
248
|
validate = options.fetch(:validate, true) || ActiveRecord::VERSION::STRING < "5.2"
|
221
249
|
|
222
250
|
if postgresql? && validate
|
223
|
-
if
|
224
|
-
raise_error :add_foreign_key_helper,
|
225
|
-
command: command_str(:add_foreign_key_safely, [from_table, to_table, options])
|
226
|
-
elsif ActiveRecord::VERSION::STRING < "5.2"
|
251
|
+
if ActiveRecord::VERSION::STRING < "5.2"
|
227
252
|
# fk name logic from rails
|
228
253
|
primary_key = options[:primary_key] || "id"
|
229
254
|
column = options[:column] || "#{to_table.to_s.singularize}_id"
|
@@ -239,6 +264,10 @@ Then add the foreign key in separate migrations."
|
|
239
264
|
validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
|
240
265
|
end
|
241
266
|
end
|
267
|
+
when :validate_foreign_key
|
268
|
+
if postgresql? && writes_blocked?
|
269
|
+
raise_error :validate_foreign_key
|
270
|
+
end
|
242
271
|
end
|
243
272
|
|
244
273
|
StrongMigrations.checks.each do |check|
|
@@ -248,9 +277,10 @@ Then add the foreign key in separate migrations."
|
|
248
277
|
|
249
278
|
result = yield
|
250
279
|
|
280
|
+
# outdated statistics + a new index can hurt performance of existing queries
|
251
281
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
252
282
|
if postgresql?
|
253
|
-
connection.execute "ANALYZE
|
283
|
+
connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}"
|
254
284
|
elsif mariadb? || mysql?
|
255
285
|
connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
|
256
286
|
end
|
@@ -259,12 +289,14 @@ Then add the foreign key in separate migrations."
|
|
259
289
|
result
|
260
290
|
end
|
261
291
|
|
292
|
+
private
|
293
|
+
|
262
294
|
def set_timeouts
|
263
295
|
if !@timeouts_set
|
264
296
|
if StrongMigrations.statement_timeout
|
265
297
|
statement =
|
266
298
|
if postgresql?
|
267
|
-
"SET statement_timeout TO #{connection.quote(StrongMigrations.statement_timeout
|
299
|
+
"SET statement_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.statement_timeout))}"
|
268
300
|
elsif mysql?
|
269
301
|
"SET max_execution_time = #{connection.quote(StrongMigrations.statement_timeout.to_i * 1000)}"
|
270
302
|
elsif mariadb?
|
@@ -279,7 +311,7 @@ Then add the foreign key in separate migrations."
|
|
279
311
|
if StrongMigrations.lock_timeout
|
280
312
|
statement =
|
281
313
|
if postgresql?
|
282
|
-
"SET lock_timeout TO #{connection.quote(StrongMigrations.lock_timeout
|
314
|
+
"SET lock_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.lock_timeout))}"
|
283
315
|
elsif mysql? || mariadb?
|
284
316
|
"SET lock_wait_timeout = #{connection.quote(StrongMigrations.lock_timeout)}"
|
285
317
|
else
|
@@ -293,8 +325,6 @@ Then add the foreign key in separate migrations."
|
|
293
325
|
end
|
294
326
|
end
|
295
327
|
|
296
|
-
private
|
297
|
-
|
298
328
|
def connection
|
299
329
|
@migration.connection
|
300
330
|
end
|
@@ -304,7 +334,8 @@ Then add the foreign key in separate migrations."
|
|
304
334
|
end
|
305
335
|
|
306
336
|
def safe?
|
307
|
-
@safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) ||
|
337
|
+
@safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) ||
|
338
|
+
(direction == :down && !StrongMigrations.check_down) || version_safe?
|
308
339
|
end
|
309
340
|
|
310
341
|
def version_safe?
|
@@ -349,8 +380,9 @@ Then add the foreign key in separate migrations."
|
|
349
380
|
end
|
350
381
|
|
351
382
|
def target_version(target_version)
|
383
|
+
target_version ||= StrongMigrations.target_version
|
352
384
|
version =
|
353
|
-
if target_version &&
|
385
|
+
if target_version && StrongMigrations.developer_env?
|
354
386
|
target_version.to_s
|
355
387
|
else
|
356
388
|
yield
|
@@ -358,8 +390,69 @@ Then add the foreign key in separate migrations."
|
|
358
390
|
Gem::Version.new(version)
|
359
391
|
end
|
360
392
|
|
361
|
-
def
|
362
|
-
StrongMigrations.
|
393
|
+
def check_lock_timeout
|
394
|
+
limit = StrongMigrations.lock_timeout_limit
|
395
|
+
|
396
|
+
if limit && !@lock_timeout_checked
|
397
|
+
if postgresql?
|
398
|
+
lock_timeout = connection.select_all("SHOW lock_timeout").first["lock_timeout"]
|
399
|
+
lock_timeout_sec = timeout_to_sec(lock_timeout)
|
400
|
+
if lock_timeout_sec == 0
|
401
|
+
warn "[strong_migrations] DANGER: No lock timeout set"
|
402
|
+
elsif lock_timeout_sec > limit
|
403
|
+
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
404
|
+
end
|
405
|
+
elsif mysql? || mariadb?
|
406
|
+
lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
|
407
|
+
if lock_timeout.to_i > limit
|
408
|
+
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
409
|
+
end
|
410
|
+
end
|
411
|
+
@lock_timeout_checked = true
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# units: https://www.postgresql.org/docs/current/config-setting.html
|
416
|
+
def timeout_to_sec(timeout)
|
417
|
+
units = {
|
418
|
+
"us" => 0.001,
|
419
|
+
"ms" => 1,
|
420
|
+
"s" => 1000,
|
421
|
+
"min" => 1000 * 60,
|
422
|
+
"h" => 1000 * 60 * 60,
|
423
|
+
"d" => 1000 * 60 * 60 * 24
|
424
|
+
}
|
425
|
+
timeout_ms = timeout.to_i
|
426
|
+
units.each do |k, v|
|
427
|
+
if timeout.end_with?(k)
|
428
|
+
timeout_ms *= v
|
429
|
+
break
|
430
|
+
end
|
431
|
+
end
|
432
|
+
timeout_ms / 1000.0
|
433
|
+
end
|
434
|
+
|
435
|
+
def postgresql_timeout(timeout)
|
436
|
+
if timeout.is_a?(String)
|
437
|
+
timeout
|
438
|
+
else
|
439
|
+
timeout.to_i * 1000
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def constraints(table_name)
|
444
|
+
query = <<~SQL
|
445
|
+
SELECT
|
446
|
+
conname AS name,
|
447
|
+
pg_get_constraintdef(oid) AS def
|
448
|
+
FROM
|
449
|
+
pg_constraint
|
450
|
+
WHERE
|
451
|
+
contype = 'c' AND
|
452
|
+
convalidated AND
|
453
|
+
conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
|
454
|
+
SQL
|
455
|
+
connection.select_all(query.squish).to_a
|
363
456
|
end
|
364
457
|
|
365
458
|
def raise_error(message_key, header: nil, append: nil, **vars)
|
@@ -406,6 +499,23 @@ Then add the foreign key in separate migrations."
|
|
406
499
|
"#{command} #{str_args.join(", ")}"
|
407
500
|
end
|
408
501
|
|
502
|
+
def writes_blocked?
|
503
|
+
query = <<~SQL
|
504
|
+
SELECT
|
505
|
+
relation::regclass::text
|
506
|
+
FROM
|
507
|
+
pg_locks
|
508
|
+
WHERE
|
509
|
+
mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
|
510
|
+
pid = pg_backend_pid()
|
511
|
+
SQL
|
512
|
+
connection.select_all(query.squish).any?
|
513
|
+
end
|
514
|
+
|
515
|
+
def rewrite_blocks
|
516
|
+
mysql? || mariadb? ? "writes" : "reads and writes"
|
517
|
+
end
|
518
|
+
|
409
519
|
def backfill_code(table, column, default)
|
410
520
|
model = table.to_s.classify
|
411
521
|
"#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
|