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