strong_migrations 0.8.0 → 1.0.0
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 +29 -0
- data/README.md +115 -93
- data/lib/strong_migrations/adapters/abstract_adapter.rb +61 -0
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +29 -0
- data/lib/strong_migrations/adapters/mysql_adapter.rb +87 -0
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +221 -0
- data/lib/strong_migrations/checker.rb +111 -511
- data/lib/strong_migrations/checks.rb +402 -0
- data/lib/strong_migrations/error_messages.rb +222 -0
- data/lib/strong_migrations/migration.rb +4 -2
- data/lib/strong_migrations/migrator.rb +19 -0
- data/lib/strong_migrations/safe_methods.rb +5 -1
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +16 -218
- metadata +10 -3
@@ -1,15 +1,16 @@
|
|
1
1
|
module StrongMigrations
|
2
2
|
class Checker
|
3
|
+
include Checks
|
3
4
|
include SafeMethods
|
4
5
|
|
5
|
-
attr_accessor :direction, :transaction_disabled
|
6
|
+
attr_accessor :direction, :transaction_disabled, :timeouts_set
|
6
7
|
|
7
8
|
def initialize(migration)
|
8
9
|
@migration = migration
|
9
10
|
@new_tables = []
|
10
11
|
@safe = false
|
11
12
|
@timeouts_set = false
|
12
|
-
@
|
13
|
+
@committed = false
|
13
14
|
end
|
14
15
|
|
15
16
|
def safety_assured
|
@@ -23,575 +24,174 @@ module StrongMigrations
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def perform(method, *args)
|
27
|
+
check_version_supported
|
26
28
|
set_timeouts
|
27
29
|
check_lock_timeout
|
28
30
|
|
29
31
|
if !safe? || safe_by_default_method?(method)
|
32
|
+
# TODO better pattern
|
33
|
+
# see checks.rb for methods
|
30
34
|
case method
|
31
|
-
when :
|
32
|
-
|
33
|
-
case method
|
34
|
-
when :remove_timestamps
|
35
|
-
["created_at", "updated_at"]
|
36
|
-
when :remove_column
|
37
|
-
[args[1].to_s]
|
38
|
-
when :remove_columns
|
39
|
-
args[1..-1].map(&:to_s)
|
40
|
-
else
|
41
|
-
options = args[2] || {}
|
42
|
-
reference = args[1]
|
43
|
-
cols = []
|
44
|
-
cols << "#{reference}_type" if options[:polymorphic]
|
45
|
-
cols << "#{reference}_id"
|
46
|
-
cols
|
47
|
-
end
|
48
|
-
|
49
|
-
code = "self.ignored_columns = #{columns.inspect}"
|
50
|
-
|
51
|
-
raise_error :remove_column,
|
52
|
-
model: args[0].to_s.classify,
|
53
|
-
code: code,
|
54
|
-
command: command_str(method, args),
|
55
|
-
column_suffix: columns.size > 1 ? "s" : ""
|
56
|
-
when :change_table
|
57
|
-
raise_error :change_table, header: "Possibly dangerous operation"
|
58
|
-
when :rename_table
|
59
|
-
raise_error :rename_table
|
60
|
-
when :rename_column
|
61
|
-
raise_error :rename_column
|
62
|
-
when :add_index
|
63
|
-
table, columns, options = args
|
64
|
-
options ||= {}
|
65
|
-
|
66
|
-
if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
|
67
|
-
raise_error :add_index_columns, header: "Best practice"
|
68
|
-
end
|
69
|
-
if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
|
70
|
-
return safe_add_index(table, columns, options) if StrongMigrations.safe_by_default
|
71
|
-
raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
|
72
|
-
end
|
73
|
-
when :remove_index
|
74
|
-
table, options = args
|
75
|
-
unless options.is_a?(Hash)
|
76
|
-
options = {column: options}
|
77
|
-
end
|
78
|
-
options ||= {}
|
79
|
-
|
80
|
-
if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
|
81
|
-
return safe_remove_index(table, options) if StrongMigrations.safe_by_default
|
82
|
-
raise_error :remove_index, command: command_str("remove_index", [table, options.merge(algorithm: :concurrently)])
|
83
|
-
end
|
35
|
+
when :add_check_constraint
|
36
|
+
check_add_check_constraint(args)
|
84
37
|
when :add_column
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
append = "
|
93
|
-
|
94
|
-
Then add the NOT NULL constraint in separate migrations."
|
95
|
-
end
|
96
|
-
|
97
|
-
raise_error :add_column_default,
|
98
|
-
add_command: command_str("add_column", [table, column, type, options.except(:default)]),
|
99
|
-
change_command: command_str("change_column_default", [table, column, default]),
|
100
|
-
remove_command: command_str("remove_column", [table, column]),
|
101
|
-
code: backfill_code(table, column, default),
|
102
|
-
append: append,
|
103
|
-
rewrite_blocks: rewrite_blocks
|
104
|
-
end
|
105
|
-
|
106
|
-
if type.to_s == "json" && postgresql?
|
107
|
-
raise_error :add_column_json,
|
108
|
-
command: command_str("add_column", [table, column, :jsonb, options])
|
109
|
-
end
|
38
|
+
check_add_column(args)
|
39
|
+
when :add_foreign_key
|
40
|
+
check_add_foreign_key(args)
|
41
|
+
when :add_index
|
42
|
+
check_add_index(args)
|
43
|
+
when :add_reference, :add_belongs_to
|
44
|
+
check_add_reference(method, args)
|
110
45
|
when :change_column
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
if existing_column
|
117
|
-
existing_type = existing_column.sql_type.split("(").first
|
118
|
-
if postgresql?
|
119
|
-
case type.to_s
|
120
|
-
when "string"
|
121
|
-
# safe to increase limit or remove it
|
122
|
-
# not safe to decrease limit or add a limit
|
123
|
-
case existing_type
|
124
|
-
when "character varying"
|
125
|
-
safe = !options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
|
126
|
-
when "text"
|
127
|
-
safe = !options[:limit]
|
128
|
-
end
|
129
|
-
when "text"
|
130
|
-
# safe to change varchar to text (and text to text)
|
131
|
-
safe = ["character varying", "text"].include?(existing_type)
|
132
|
-
when "numeric", "decimal"
|
133
|
-
# numeric and decimal are equivalent and can be used interchangably
|
134
|
-
safe = ["numeric", "decimal"].include?(existing_type) &&
|
135
|
-
(
|
136
|
-
(
|
137
|
-
# unconstrained
|
138
|
-
!options[:precision] && !options[:scale]
|
139
|
-
) || (
|
140
|
-
# increased precision, same scale
|
141
|
-
options[:precision] && existing_column.precision &&
|
142
|
-
options[:precision] >= existing_column.precision &&
|
143
|
-
options[:scale] == existing_column.scale
|
144
|
-
)
|
145
|
-
)
|
146
|
-
when "datetime", "timestamp", "timestamptz"
|
147
|
-
safe = ["timestamp without time zone", "timestamp with time zone"].include?(existing_type) &&
|
148
|
-
postgresql_version >= Gem::Version.new("12") &&
|
149
|
-
connection.select_all("SHOW timezone").first["TimeZone"] == "UTC"
|
150
|
-
end
|
151
|
-
elsif mysql? || mariadb?
|
152
|
-
case type.to_s
|
153
|
-
when "string"
|
154
|
-
# https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html
|
155
|
-
# https://mariadb.com/kb/en/innodb-online-ddl-operations-with-the-instant-alter-algorithm/#changing-the-data-type-of-a-column
|
156
|
-
# increased limit, but doesn't change number of length bytes
|
157
|
-
# 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
|
158
|
-
limit = options[:limit] || 255
|
159
|
-
safe = ["varchar"].include?(existing_type) &&
|
160
|
-
limit >= existing_column.limit &&
|
161
|
-
(limit <= 255 || existing_column.limit > 255)
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
# unsafe to set NOT NULL for safe types
|
167
|
-
if safe && existing_column.null && options[:null] == false
|
168
|
-
raise_error :change_column_with_not_null
|
169
|
-
end
|
170
|
-
|
171
|
-
raise_error :change_column, rewrite_blocks: rewrite_blocks unless safe
|
46
|
+
check_change_column(args)
|
47
|
+
when :change_column_null
|
48
|
+
check_change_column_null(args)
|
49
|
+
when :change_table
|
50
|
+
check_change_table
|
172
51
|
when :create_table
|
173
|
-
|
174
|
-
options ||= {}
|
175
|
-
|
176
|
-
raise_error :create_table if options[:force]
|
177
|
-
|
178
|
-
# keep track of new tables of add_index check
|
179
|
-
@new_tables << table.to_s
|
180
|
-
when :add_reference, :add_belongs_to
|
181
|
-
table, reference, options = args
|
182
|
-
options ||= {}
|
183
|
-
|
184
|
-
if postgresql?
|
185
|
-
index_value = options.fetch(:index, true)
|
186
|
-
concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently
|
187
|
-
bad_index = index_value && !concurrently_set
|
188
|
-
|
189
|
-
if bad_index || options[:foreign_key]
|
190
|
-
if index_value.is_a?(Hash)
|
191
|
-
options[:index] = options[:index].merge(algorithm: :concurrently)
|
192
|
-
else
|
193
|
-
options = options.merge(index: {algorithm: :concurrently})
|
194
|
-
end
|
195
|
-
|
196
|
-
return safe_add_reference(table, reference, options) if StrongMigrations.safe_by_default
|
197
|
-
|
198
|
-
if options.delete(:foreign_key)
|
199
|
-
headline = "Adding a foreign key blocks writes on both tables."
|
200
|
-
append = "
|
201
|
-
|
202
|
-
Then add the foreign key in separate migrations."
|
203
|
-
else
|
204
|
-
headline = "Adding an index non-concurrently locks the table."
|
205
|
-
end
|
206
|
-
|
207
|
-
raise_error :add_reference,
|
208
|
-
headline: headline,
|
209
|
-
command: command_str(method, [table, reference, options]),
|
210
|
-
append: append
|
211
|
-
end
|
212
|
-
end
|
52
|
+
check_create_table(args)
|
213
53
|
when :execute
|
214
|
-
|
215
|
-
when :
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
unless safe
|
225
|
-
# match https://github.com/nullobject/rein
|
226
|
-
constraint_name = "#{table}_#{column}_null"
|
227
|
-
|
228
|
-
add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
|
229
|
-
validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
|
230
|
-
remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
|
231
|
-
|
232
|
-
constraint_methods = ar_version >= 6.1
|
233
|
-
|
234
|
-
validate_constraint_code =
|
235
|
-
if constraint_methods
|
236
|
-
String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
|
237
|
-
else
|
238
|
-
String.new(safety_assured_str(validate_code))
|
239
|
-
end
|
240
|
-
|
241
|
-
if postgresql_version >= Gem::Version.new("12")
|
242
|
-
change_args = [table, column, null]
|
243
|
-
|
244
|
-
validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
|
245
|
-
|
246
|
-
if constraint_methods
|
247
|
-
validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
|
248
|
-
else
|
249
|
-
validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
return safe_change_column_null(add_code, validate_code, change_args, remove_code) if StrongMigrations.safe_by_default
|
254
|
-
|
255
|
-
add_constraint_code =
|
256
|
-
if constraint_methods
|
257
|
-
# only quote when needed
|
258
|
-
expr_column = column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column)
|
259
|
-
command_str(:add_check_constraint, [table, "#{expr_column} IS NOT NULL", {name: constraint_name, validate: false}])
|
260
|
-
else
|
261
|
-
safety_assured_str(add_code)
|
262
|
-
end
|
263
|
-
|
264
|
-
raise_error :change_column_null_postgresql,
|
265
|
-
add_constraint_code: add_constraint_code,
|
266
|
-
validate_constraint_code: validate_constraint_code
|
267
|
-
end
|
268
|
-
elsif mysql? || mariadb?
|
269
|
-
raise_error :change_column_null_mysql
|
270
|
-
elsif !default.nil?
|
271
|
-
raise_error :change_column_null,
|
272
|
-
code: backfill_code(table, column, default)
|
273
|
-
end
|
274
|
-
end
|
275
|
-
when :add_foreign_key
|
276
|
-
from_table, to_table, options = args
|
277
|
-
options ||= {}
|
278
|
-
|
279
|
-
validate = options.fetch(:validate, true)
|
280
|
-
|
281
|
-
if postgresql? && validate
|
282
|
-
return safe_add_foreign_key(from_table, to_table, options) if StrongMigrations.safe_by_default
|
283
|
-
|
284
|
-
raise_error :add_foreign_key,
|
285
|
-
add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
|
286
|
-
validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
|
287
|
-
end
|
288
|
-
when :validate_foreign_key
|
289
|
-
if postgresql? && writes_blocked?
|
290
|
-
raise_error :validate_foreign_key
|
291
|
-
end
|
292
|
-
when :add_check_constraint
|
293
|
-
table, expression, options = args
|
294
|
-
options ||= {}
|
295
|
-
|
296
|
-
if !new_table?(table)
|
297
|
-
if postgresql? && options[:validate] != false
|
298
|
-
add_options = options.merge(validate: false)
|
299
|
-
name = options[:name] || @migration.check_constraint_options(table, expression, options)[:name]
|
300
|
-
validate_options = {name: name}
|
301
|
-
|
302
|
-
return safe_add_check_constraint(table, expression, add_options, validate_options) if StrongMigrations.safe_by_default
|
303
|
-
|
304
|
-
raise_error :add_check_constraint,
|
305
|
-
add_check_constraint_code: command_str("add_check_constraint", [table, expression, add_options]),
|
306
|
-
validate_check_constraint_code: command_str("validate_check_constraint", [table, validate_options])
|
307
|
-
elsif mysql? || mariadb?
|
308
|
-
raise_error :add_check_constraint_mysql
|
309
|
-
end
|
310
|
-
end
|
54
|
+
check_execute
|
55
|
+
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
|
56
|
+
check_remove_column(method, args)
|
57
|
+
when :remove_index
|
58
|
+
check_remove_index(args)
|
59
|
+
when :rename_column
|
60
|
+
check_rename_column
|
61
|
+
when :rename_table
|
62
|
+
check_rename_table
|
311
63
|
when :validate_check_constraint
|
312
|
-
|
313
|
-
|
314
|
-
|
64
|
+
check_validate_check_constraint
|
65
|
+
when :validate_foreign_key
|
66
|
+
check_validate_foreign_key
|
67
|
+
when :commit_db_transaction
|
68
|
+
# if committed, likely no longer in DDL transaction
|
69
|
+
# and no longer eligible to be retried at migration level
|
70
|
+
# okay to have false positives
|
71
|
+
@committed = true
|
315
72
|
end
|
316
73
|
|
74
|
+
# custom checks
|
317
75
|
StrongMigrations.checks.each do |check|
|
318
76
|
@migration.instance_exec(method, args, &check)
|
319
77
|
end
|
320
78
|
end
|
321
79
|
|
322
|
-
result =
|
80
|
+
result =
|
81
|
+
if retry_lock_timeouts?(method)
|
82
|
+
# TODO figure out how to handle methods that generate multiple statements
|
83
|
+
# like add_reference(table, ref, index: {algorithm: :concurrently})
|
84
|
+
# lock timeout after first statement will cause retry to fail
|
85
|
+
retry_lock_timeouts { yield }
|
86
|
+
else
|
87
|
+
yield
|
88
|
+
end
|
323
89
|
|
324
90
|
# outdated statistics + a new index can hurt performance of existing queries
|
325
91
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
326
|
-
analyze_table(args[0])
|
92
|
+
adapter.analyze_table(args[0])
|
327
93
|
end
|
328
94
|
|
329
95
|
result
|
330
96
|
end
|
331
97
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
elsif mariadb?
|
344
|
-
"SET max_statement_time = #{connection.quote(StrongMigrations.statement_timeout)}"
|
345
|
-
else
|
346
|
-
raise StrongMigrations::Error, "Statement timeout not supported for this database"
|
347
|
-
end
|
348
|
-
|
349
|
-
connection.select_all(statement)
|
350
|
-
end
|
351
|
-
|
352
|
-
if StrongMigrations.lock_timeout
|
353
|
-
statement =
|
354
|
-
if postgresql?
|
355
|
-
"SET lock_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.lock_timeout))}"
|
356
|
-
elsif mysql? || mariadb?
|
357
|
-
"SET lock_wait_timeout = #{connection.quote(StrongMigrations.lock_timeout)}"
|
358
|
-
else
|
359
|
-
raise StrongMigrations::Error, "Lock timeout not supported for this database"
|
360
|
-
end
|
361
|
-
|
362
|
-
connection.select_all(statement)
|
98
|
+
def retry_lock_timeouts(check_committed: false)
|
99
|
+
retries = 0
|
100
|
+
begin
|
101
|
+
yield
|
102
|
+
rescue ActiveRecord::LockWaitTimeout => e
|
103
|
+
if retries < StrongMigrations.lock_timeout_retries && !(check_committed && @committed)
|
104
|
+
retries += 1
|
105
|
+
delay = StrongMigrations.lock_timeout_retry_delay
|
106
|
+
@migration.say("Lock timeout. Retrying in #{delay} seconds...")
|
107
|
+
sleep(delay)
|
108
|
+
retry
|
363
109
|
end
|
364
|
-
|
365
|
-
@timeouts_set = true
|
110
|
+
raise e
|
366
111
|
end
|
367
112
|
end
|
368
113
|
|
369
|
-
|
370
|
-
@migration.connection
|
371
|
-
end
|
372
|
-
|
373
|
-
def version
|
374
|
-
@migration.version
|
375
|
-
end
|
376
|
-
|
377
|
-
def safe?
|
378
|
-
@safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
|
379
|
-
end
|
380
|
-
|
381
|
-
def version_safe?
|
382
|
-
version && version <= StrongMigrations.start_after
|
383
|
-
end
|
114
|
+
private
|
384
115
|
|
385
|
-
def
|
386
|
-
|
387
|
-
end
|
116
|
+
def check_version_supported
|
117
|
+
return if defined?(@version_checked)
|
388
118
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
119
|
+
min_version = adapter.min_version
|
120
|
+
if min_version
|
121
|
+
version = adapter.server_version
|
122
|
+
if version < Gem::Version.new(min_version)
|
123
|
+
raise UnsupportedVersion, "#{adapter.name} version (#{version}) not supported in this version of Strong Migrations (#{StrongMigrations::VERSION})"
|
394
124
|
end
|
395
125
|
end
|
396
|
-
end
|
397
126
|
|
398
|
-
|
399
|
-
connection.adapter_name =~ /mysql/i && !connection.try(:mariadb?)
|
127
|
+
@version_checked = true
|
400
128
|
end
|
401
129
|
|
402
|
-
def
|
403
|
-
|
404
|
-
target_version(StrongMigrations.target_mysql_version) do
|
405
|
-
connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
406
|
-
end
|
407
|
-
end
|
408
|
-
end
|
409
|
-
|
410
|
-
def mariadb?
|
411
|
-
connection.adapter_name =~ /mysql/i && connection.try(:mariadb?)
|
412
|
-
end
|
130
|
+
def set_timeouts
|
131
|
+
return if @timeouts_set
|
413
132
|
|
414
|
-
|
415
|
-
|
416
|
-
target_version(StrongMigrations.target_mariadb_version) do
|
417
|
-
connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
418
|
-
end
|
133
|
+
if StrongMigrations.statement_timeout
|
134
|
+
adapter.set_statement_timeout(StrongMigrations.statement_timeout)
|
419
135
|
end
|
420
|
-
|
421
|
-
|
422
|
-
def target_version(target_version)
|
423
|
-
target_version ||= StrongMigrations.target_version
|
424
|
-
version =
|
425
|
-
if target_version && StrongMigrations.developer_env?
|
426
|
-
target_version.to_s
|
427
|
-
else
|
428
|
-
yield
|
429
|
-
end
|
430
|
-
Gem::Version.new(version)
|
431
|
-
end
|
432
|
-
|
433
|
-
def ar_version
|
434
|
-
ActiveRecord::VERSION::STRING.to_f
|
435
|
-
end
|
436
|
-
|
437
|
-
def check_lock_timeout
|
438
|
-
limit = StrongMigrations.lock_timeout_limit
|
439
|
-
|
440
|
-
if limit && !@lock_timeout_checked
|
441
|
-
if postgresql?
|
442
|
-
lock_timeout = connection.select_all("SHOW lock_timeout").first["lock_timeout"]
|
443
|
-
lock_timeout_sec = timeout_to_sec(lock_timeout)
|
444
|
-
if lock_timeout_sec == 0
|
445
|
-
warn "[strong_migrations] DANGER: No lock timeout set"
|
446
|
-
elsif lock_timeout_sec > limit
|
447
|
-
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
448
|
-
end
|
449
|
-
elsif mysql? || mariadb?
|
450
|
-
lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
|
451
|
-
# lock timeout is an integer
|
452
|
-
if lock_timeout.to_i > limit
|
453
|
-
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
454
|
-
end
|
455
|
-
end
|
456
|
-
@lock_timeout_checked = true
|
136
|
+
if StrongMigrations.lock_timeout
|
137
|
+
adapter.set_lock_timeout(StrongMigrations.lock_timeout)
|
457
138
|
end
|
458
|
-
end
|
459
139
|
|
460
|
-
|
461
|
-
def timeout_to_sec(timeout)
|
462
|
-
units = {
|
463
|
-
"us" => 0.001,
|
464
|
-
"ms" => 1,
|
465
|
-
"s" => 1000,
|
466
|
-
"min" => 1000 * 60,
|
467
|
-
"h" => 1000 * 60 * 60,
|
468
|
-
"d" => 1000 * 60 * 60 * 24
|
469
|
-
}
|
470
|
-
timeout_ms = timeout.to_i
|
471
|
-
units.each do |k, v|
|
472
|
-
if timeout.end_with?(k)
|
473
|
-
timeout_ms *= v
|
474
|
-
break
|
475
|
-
end
|
476
|
-
end
|
477
|
-
timeout_ms / 1000.0
|
140
|
+
@timeouts_set = true
|
478
141
|
end
|
479
142
|
|
480
|
-
def
|
481
|
-
if
|
482
|
-
timeout
|
483
|
-
else
|
484
|
-
# use ceil to prevent no timeout for values under 1 ms
|
485
|
-
(timeout.to_f * 1000).ceil
|
486
|
-
end
|
487
|
-
end
|
143
|
+
def check_lock_timeout
|
144
|
+
return if defined?(@lock_timeout_checked)
|
488
145
|
|
489
|
-
|
490
|
-
|
491
|
-
connection.execute "ANALYZE #{connection.quote_table_name(table.to_s)}"
|
492
|
-
elsif mariadb? || mysql?
|
493
|
-
connection.execute "ANALYZE TABLE #{connection.quote_table_name(table.to_s)}"
|
146
|
+
if StrongMigrations.lock_timeout_limit
|
147
|
+
adapter.check_lock_timeout(StrongMigrations.lock_timeout_limit)
|
494
148
|
end
|
495
|
-
end
|
496
149
|
|
497
|
-
|
498
|
-
query = <<~SQL
|
499
|
-
SELECT
|
500
|
-
conname AS name,
|
501
|
-
pg_get_constraintdef(oid) AS def
|
502
|
-
FROM
|
503
|
-
pg_constraint
|
504
|
-
WHERE
|
505
|
-
contype = 'c' AND
|
506
|
-
convalidated AND
|
507
|
-
conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
|
508
|
-
SQL
|
509
|
-
connection.select_all(query.squish).to_a
|
150
|
+
@lock_timeout_checked = true
|
510
151
|
end
|
511
152
|
|
512
|
-
def
|
513
|
-
|
514
|
-
|
515
|
-
message = StrongMigrations.error_messages[message_key] || "Missing message"
|
516
|
-
message = message + append if append
|
517
|
-
|
518
|
-
vars[:migration_name] = @migration.class.name
|
519
|
-
vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
520
|
-
vars[:base_model] = "ApplicationRecord"
|
521
|
-
|
522
|
-
# escape % not followed by {
|
523
|
-
message = message.gsub(/%(?!{)/, "%%") % vars if message.include?("%")
|
524
|
-
@migration.stop!(message, header: header || "Dangerous operation detected")
|
153
|
+
def safe?
|
154
|
+
@safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
|
525
155
|
end
|
526
156
|
|
527
|
-
def
|
528
|
-
|
529
|
-
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
157
|
+
def version_safe?
|
158
|
+
version && version <= StrongMigrations.start_after
|
530
159
|
end
|
531
160
|
|
532
|
-
def
|
533
|
-
|
161
|
+
def version
|
162
|
+
@migration.version
|
534
163
|
end
|
535
164
|
|
536
|
-
def
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
# pretty index: {algorithm: :concurrently}
|
546
|
-
"#{k}: {#{v.map { |k2, v2| "#{k2}: #{v2.inspect}" }.join(", ")}}"
|
165
|
+
def adapter
|
166
|
+
@adapter ||= begin
|
167
|
+
cls =
|
168
|
+
case connection.adapter_name
|
169
|
+
when /postg/i # PostgreSQL, PostGIS
|
170
|
+
Adapters::PostgreSQLAdapter
|
171
|
+
when /mysql/i
|
172
|
+
if connection.try(:mariadb?)
|
173
|
+
Adapters::MariaDBAdapter
|
547
174
|
else
|
548
|
-
|
175
|
+
Adapters::MySQLAdapter
|
549
176
|
end
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
str_args << last_arg.inspect
|
554
|
-
end
|
555
|
-
|
556
|
-
"#{command} #{str_args.join(", ")}"
|
557
|
-
end
|
558
|
-
|
559
|
-
def writes_blocked?
|
560
|
-
query = <<~SQL
|
561
|
-
SELECT
|
562
|
-
relation::regclass::text
|
563
|
-
FROM
|
564
|
-
pg_locks
|
565
|
-
WHERE
|
566
|
-
mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
|
567
|
-
pid = pg_backend_pid()
|
568
|
-
SQL
|
569
|
-
connection.select_all(query.squish).any?
|
570
|
-
end
|
571
|
-
|
572
|
-
def rewrite_blocks
|
573
|
-
mysql? || mariadb? ? "writes" : "reads and writes"
|
574
|
-
end
|
177
|
+
else
|
178
|
+
Adapters::AbstractAdapter
|
179
|
+
end
|
575
180
|
|
576
|
-
|
577
|
-
|
578
|
-
"#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
|
181
|
+
cls.new(self)
|
182
|
+
end
|
579
183
|
end
|
580
184
|
|
581
|
-
def
|
582
|
-
@
|
185
|
+
def connection
|
186
|
+
@migration.connection
|
583
187
|
end
|
584
188
|
|
585
|
-
def
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
mariadb_version >= Gem::Version.new("10.3.2")
|
592
|
-
else
|
593
|
-
false
|
594
|
-
end
|
189
|
+
def retry_lock_timeouts?(method)
|
190
|
+
(
|
191
|
+
StrongMigrations.lock_timeout_retries > 0 &&
|
192
|
+
!in_transaction? &&
|
193
|
+
method != :transaction
|
194
|
+
)
|
595
195
|
end
|
596
196
|
end
|
597
197
|
end
|