strong_migrations 0.7.9 → 1.4.4
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 +80 -0
- data/LICENSE.txt +1 -1
- data/README.md +143 -126
- data/lib/strong_migrations/adapters/abstract_adapter.rb +77 -0
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +32 -0
- data/lib/strong_migrations/adapters/mysql_adapter.rb +107 -0
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +230 -0
- data/lib/strong_migrations/checker.rb +115 -516
- data/lib/strong_migrations/checks.rb +460 -0
- data/lib/strong_migrations/error_messages.rb +240 -0
- data/lib/strong_migrations/migration.rb +7 -2
- data/lib/strong_migrations/migrator.rb +19 -0
- data/lib/strong_migrations/safe_methods.rb +27 -28
- data/lib/strong_migrations/schema_dumper.rb +21 -0
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +38 -225
- data/lib/tasks/strong_migrations.rake +2 -7
- metadata +14 -7
- data/lib/strong_migrations/alphabetize_columns.rb +0 -11
@@ -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,178 @@ 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
|
-
|
93
|
-
|
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_exclusion_constraint
|
40
|
+
check_add_exclusion_constraint(*args)
|
41
|
+
when :add_foreign_key
|
42
|
+
check_add_foreign_key(*args)
|
43
|
+
when :add_index
|
44
|
+
check_add_index(*args)
|
45
|
+
when :add_reference, :add_belongs_to
|
46
|
+
check_add_reference(method, *args)
|
111
47
|
when :change_column
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
48
|
+
check_change_column(*args)
|
49
|
+
when :change_column_null
|
50
|
+
check_change_column_null(*args)
|
51
|
+
when :change_table
|
52
|
+
check_change_table
|
53
|
+
when :create_join_table
|
54
|
+
check_create_join_table(*args)
|
173
55
|
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
|
56
|
+
check_create_table(*args)
|
214
57
|
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
|
58
|
+
check_execute
|
59
|
+
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
|
60
|
+
check_remove_column(method, *args)
|
61
|
+
when :remove_index
|
62
|
+
check_remove_index(*args)
|
63
|
+
when :rename_column
|
64
|
+
check_rename_column
|
65
|
+
when :rename_table
|
66
|
+
check_rename_table
|
328
67
|
when :validate_check_constraint
|
329
|
-
|
330
|
-
|
331
|
-
|
68
|
+
check_validate_check_constraint
|
69
|
+
when :validate_foreign_key
|
70
|
+
check_validate_foreign_key
|
71
|
+
when :commit_db_transaction
|
72
|
+
# if committed, likely no longer in DDL transaction
|
73
|
+
# and no longer eligible to be retried at migration level
|
74
|
+
# okay to have false positives
|
75
|
+
@committed = true
|
332
76
|
end
|
333
77
|
|
78
|
+
# custom checks
|
334
79
|
StrongMigrations.checks.each do |check|
|
335
80
|
@migration.instance_exec(method, args, &check)
|
336
81
|
end
|
337
82
|
end
|
338
83
|
|
339
|
-
result =
|
84
|
+
result =
|
85
|
+
if retry_lock_timeouts?(method)
|
86
|
+
# TODO figure out how to handle methods that generate multiple statements
|
87
|
+
# like add_reference(table, ref, index: {algorithm: :concurrently})
|
88
|
+
# lock timeout after first statement will cause retry to fail
|
89
|
+
retry_lock_timeouts { yield }
|
90
|
+
else
|
91
|
+
yield
|
92
|
+
end
|
340
93
|
|
341
94
|
# outdated statistics + a new index can hurt performance of existing queries
|
342
95
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
343
|
-
analyze_table(args[0])
|
96
|
+
adapter.analyze_table(args[0])
|
344
97
|
end
|
345
98
|
|
346
99
|
result
|
347
100
|
end
|
348
101
|
|
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)
|
102
|
+
def retry_lock_timeouts(check_committed: false)
|
103
|
+
retries = 0
|
104
|
+
begin
|
105
|
+
yield
|
106
|
+
rescue ActiveRecord::LockWaitTimeout => e
|
107
|
+
if retries < StrongMigrations.lock_timeout_retries && !(check_committed && @committed)
|
108
|
+
retries += 1
|
109
|
+
delay = StrongMigrations.lock_timeout_retry_delay
|
110
|
+
@migration.say("Lock timeout. Retrying in #{delay} seconds...")
|
111
|
+
sleep(delay)
|
112
|
+
retry
|
380
113
|
end
|
381
|
-
|
382
|
-
@timeouts_set = true
|
114
|
+
raise e
|
383
115
|
end
|
384
116
|
end
|
385
117
|
|
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
|
118
|
+
private
|
401
119
|
|
402
|
-
def
|
403
|
-
|
404
|
-
end
|
120
|
+
def check_version_supported
|
121
|
+
return if defined?(@version_checked)
|
405
122
|
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
123
|
+
min_version = adapter.min_version
|
124
|
+
if min_version
|
125
|
+
version = adapter.server_version
|
126
|
+
if version < Gem::Version.new(min_version)
|
127
|
+
raise UnsupportedVersion, "#{adapter.name} version (#{version}) not supported in this version of Strong Migrations (#{StrongMigrations::VERSION})"
|
411
128
|
end
|
412
129
|
end
|
413
|
-
end
|
414
|
-
|
415
|
-
def mysql?
|
416
|
-
connection.adapter_name =~ /mysql/i && !connection.try(:mariadb?)
|
417
|
-
end
|
418
130
|
|
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
|
131
|
+
@version_checked = true
|
425
132
|
end
|
426
133
|
|
427
|
-
def
|
428
|
-
|
429
|
-
end
|
134
|
+
def set_timeouts
|
135
|
+
return if @timeouts_set
|
430
136
|
|
431
|
-
|
432
|
-
|
433
|
-
target_version(StrongMigrations.target_mariadb_version) do
|
434
|
-
connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
435
|
-
end
|
137
|
+
if StrongMigrations.statement_timeout
|
138
|
+
adapter.set_statement_timeout(StrongMigrations.statement_timeout)
|
436
139
|
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
|
140
|
+
if StrongMigrations.lock_timeout
|
141
|
+
adapter.set_lock_timeout(StrongMigrations.lock_timeout)
|
474
142
|
end
|
475
|
-
end
|
476
143
|
|
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
|
144
|
+
@timeouts_set = true
|
495
145
|
end
|
496
146
|
|
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
|
147
|
+
def check_lock_timeout
|
148
|
+
return if defined?(@lock_timeout_checked)
|
505
149
|
|
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)}"
|
150
|
+
if StrongMigrations.lock_timeout_limit
|
151
|
+
adapter.check_lock_timeout(StrongMigrations.lock_timeout_limit)
|
511
152
|
end
|
512
|
-
end
|
513
153
|
|
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
|
154
|
+
@lock_timeout_checked = true
|
527
155
|
end
|
528
156
|
|
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")
|
157
|
+
def safe?
|
158
|
+
@safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
|
542
159
|
end
|
543
160
|
|
544
|
-
def
|
545
|
-
|
546
|
-
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
161
|
+
def version_safe?
|
162
|
+
version && version <= StrongMigrations.start_after
|
547
163
|
end
|
548
164
|
|
549
|
-
def
|
550
|
-
|
165
|
+
def version
|
166
|
+
@migration.version
|
551
167
|
end
|
552
168
|
|
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(", ")}}"
|
169
|
+
def adapter
|
170
|
+
@adapter ||= begin
|
171
|
+
cls =
|
172
|
+
case connection.adapter_name
|
173
|
+
when /postg/i # PostgreSQL, PostGIS
|
174
|
+
Adapters::PostgreSQLAdapter
|
175
|
+
when /mysql/i
|
176
|
+
if connection.try(:mariadb?)
|
177
|
+
Adapters::MariaDBAdapter
|
564
178
|
else
|
565
|
-
|
179
|
+
Adapters::MySQLAdapter
|
566
180
|
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
|
181
|
+
else
|
182
|
+
Adapters::AbstractAdapter
|
183
|
+
end
|
588
184
|
|
589
|
-
|
590
|
-
|
185
|
+
cls.new(self)
|
186
|
+
end
|
591
187
|
end
|
592
188
|
|
593
|
-
def
|
594
|
-
|
595
|
-
"#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
|
189
|
+
def connection
|
190
|
+
@migration.connection
|
596
191
|
end
|
597
192
|
|
598
|
-
def
|
599
|
-
|
193
|
+
def retry_lock_timeouts?(method)
|
194
|
+
(
|
195
|
+
StrongMigrations.lock_timeout_retries > 0 &&
|
196
|
+
!in_transaction? &&
|
197
|
+
method != :transaction
|
198
|
+
)
|
600
199
|
end
|
601
200
|
end
|
602
201
|
end
|