strong_migrations 0.7.6 → 1.7.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 +123 -1
- data/LICENSE.txt +1 -1
- data/README.md +255 -147
- 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 +131 -519
- data/lib/strong_migrations/checks.rb +498 -0
- data/lib/strong_migrations/database_tasks.rb +2 -1
- data/lib/strong_migrations/error_messages.rb +267 -0
- data/lib/strong_migrations/migration.rb +18 -3
- data/lib/strong_migrations/migrator.rb +19 -0
- data/lib/strong_migrations/safe_methods.rb +33 -29
- 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 +15 -78
- data/lib/strong_migrations/alphabetize_columns.rb +0 -11
@@ -1,599 +1,211 @@
|
|
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
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :safe
|
10
|
+
end
|
6
11
|
|
7
12
|
def initialize(migration)
|
8
13
|
@migration = migration
|
9
14
|
@new_tables = []
|
10
|
-
@
|
15
|
+
@new_columns = []
|
11
16
|
@timeouts_set = false
|
12
|
-
@
|
17
|
+
@committed = false
|
13
18
|
end
|
14
19
|
|
15
|
-
def safety_assured
|
16
|
-
previous_value =
|
20
|
+
def self.safety_assured
|
21
|
+
previous_value = safe
|
17
22
|
begin
|
18
|
-
|
23
|
+
self.safe = true
|
19
24
|
yield
|
20
25
|
ensure
|
21
|
-
|
26
|
+
self.safe = previous_value
|
22
27
|
end
|
23
28
|
end
|
24
29
|
|
25
30
|
def perform(method, *args)
|
31
|
+
check_version_supported
|
26
32
|
set_timeouts
|
27
33
|
check_lock_timeout
|
28
34
|
|
29
35
|
if !safe? || safe_by_default_method?(method)
|
36
|
+
# TODO better pattern
|
37
|
+
# see checks.rb for methods
|
30
38
|
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
|
39
|
+
when :add_check_constraint
|
40
|
+
check_add_check_constraint(*args)
|
84
41
|
when :add_column
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
42
|
+
check_add_column(*args)
|
43
|
+
when :add_exclusion_constraint
|
44
|
+
check_add_exclusion_constraint(*args)
|
45
|
+
when :add_foreign_key
|
46
|
+
check_add_foreign_key(*args)
|
47
|
+
when :add_index
|
48
|
+
check_add_index(*args)
|
49
|
+
when :add_reference, :add_belongs_to
|
50
|
+
check_add_reference(method, *args)
|
51
|
+
when :add_unique_constraint
|
52
|
+
check_add_unique_constraint(*args)
|
111
53
|
when :change_column
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
54
|
+
check_change_column(*args)
|
55
|
+
when :change_column_default
|
56
|
+
check_change_column_default(*args)
|
57
|
+
when :change_column_null
|
58
|
+
check_change_column_null(*args)
|
59
|
+
when :change_table
|
60
|
+
check_change_table
|
61
|
+
when :create_join_table
|
62
|
+
check_create_join_table(*args)
|
173
63
|
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
|
64
|
+
check_create_table(*args)
|
214
65
|
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
|
66
|
+
check_execute
|
67
|
+
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
|
68
|
+
check_remove_column(method, *args)
|
69
|
+
when :remove_index
|
70
|
+
check_remove_index(*args)
|
71
|
+
when :rename_column
|
72
|
+
check_rename_column
|
73
|
+
when :rename_table
|
74
|
+
check_rename_table
|
328
75
|
when :validate_check_constraint
|
329
|
-
|
330
|
-
|
331
|
-
|
76
|
+
check_validate_check_constraint
|
77
|
+
when :validate_foreign_key
|
78
|
+
check_validate_foreign_key
|
79
|
+
when :commit_db_transaction
|
80
|
+
# if committed, likely no longer in DDL transaction
|
81
|
+
# and no longer eligible to be retried at migration level
|
82
|
+
# okay to have false positives
|
83
|
+
@committed = true
|
332
84
|
end
|
333
85
|
|
334
|
-
|
335
|
-
|
86
|
+
if !safe?
|
87
|
+
# custom checks
|
88
|
+
StrongMigrations.checks.each do |check|
|
89
|
+
@migration.instance_exec(method, args, &check)
|
90
|
+
end
|
336
91
|
end
|
337
92
|
end
|
338
93
|
|
339
|
-
result =
|
94
|
+
result =
|
95
|
+
if retry_lock_timeouts?(method)
|
96
|
+
# TODO figure out how to handle methods that generate multiple statements
|
97
|
+
# like add_reference(table, ref, index: {algorithm: :concurrently})
|
98
|
+
# lock timeout after first statement will cause retry to fail
|
99
|
+
retry_lock_timeouts { yield }
|
100
|
+
else
|
101
|
+
yield
|
102
|
+
end
|
340
103
|
|
341
104
|
# outdated statistics + a new index can hurt performance of existing queries
|
342
105
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
343
|
-
|
344
|
-
connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}"
|
345
|
-
elsif mariadb? || mysql?
|
346
|
-
connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
|
347
|
-
end
|
106
|
+
adapter.analyze_table(args[0])
|
348
107
|
end
|
349
108
|
|
350
109
|
result
|
351
110
|
end
|
352
111
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
elsif mariadb?
|
365
|
-
"SET max_statement_time = #{connection.quote(StrongMigrations.statement_timeout)}"
|
366
|
-
else
|
367
|
-
raise StrongMigrations::Error, "Statement timeout not supported for this database"
|
368
|
-
end
|
369
|
-
|
370
|
-
connection.select_all(statement)
|
371
|
-
end
|
372
|
-
|
373
|
-
if StrongMigrations.lock_timeout
|
374
|
-
statement =
|
375
|
-
if postgresql?
|
376
|
-
"SET lock_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.lock_timeout))}"
|
377
|
-
elsif mysql? || mariadb?
|
378
|
-
"SET lock_wait_timeout = #{connection.quote(StrongMigrations.lock_timeout)}"
|
379
|
-
else
|
380
|
-
raise StrongMigrations::Error, "Lock timeout not supported for this database"
|
381
|
-
end
|
382
|
-
|
383
|
-
connection.select_all(statement)
|
112
|
+
def retry_lock_timeouts(check_committed: false)
|
113
|
+
retries = 0
|
114
|
+
begin
|
115
|
+
yield
|
116
|
+
rescue ActiveRecord::LockWaitTimeout => e
|
117
|
+
if retries < StrongMigrations.lock_timeout_retries && !(check_committed && @committed)
|
118
|
+
retries += 1
|
119
|
+
delay = StrongMigrations.lock_timeout_retry_delay
|
120
|
+
@migration.say("Lock timeout. Retrying in #{delay} seconds...")
|
121
|
+
sleep(delay)
|
122
|
+
retry
|
384
123
|
end
|
385
|
-
|
386
|
-
@timeouts_set = true
|
124
|
+
raise e
|
387
125
|
end
|
388
126
|
end
|
389
127
|
|
390
|
-
def connection
|
391
|
-
@migration.connection
|
392
|
-
end
|
393
|
-
|
394
|
-
def version
|
395
|
-
@migration.version
|
396
|
-
end
|
397
|
-
|
398
|
-
def safe?
|
399
|
-
@safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) ||
|
400
|
-
(direction == :down && !StrongMigrations.check_down) || version_safe?
|
401
|
-
end
|
402
|
-
|
403
128
|
def version_safe?
|
404
129
|
version && version <= StrongMigrations.start_after
|
405
130
|
end
|
406
131
|
|
407
|
-
|
408
|
-
connection.adapter_name =~ /postg/i # PostgreSQL, PostGIS
|
409
|
-
end
|
410
|
-
|
411
|
-
def postgresql_version
|
412
|
-
@postgresql_version ||= begin
|
413
|
-
target_version(StrongMigrations.target_postgresql_version) do
|
414
|
-
# only works with major versions
|
415
|
-
connection.select_all("SHOW server_version_num").first["server_version_num"].to_i / 10000
|
416
|
-
end
|
417
|
-
end
|
418
|
-
end
|
419
|
-
|
420
|
-
def mysql?
|
421
|
-
connection.adapter_name =~ /mysql/i && !connection.try(:mariadb?)
|
422
|
-
end
|
423
|
-
|
424
|
-
def mysql_version
|
425
|
-
@mysql_version ||= begin
|
426
|
-
target_version(StrongMigrations.target_mysql_version) do
|
427
|
-
connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
428
|
-
end
|
429
|
-
end
|
430
|
-
end
|
132
|
+
private
|
431
133
|
|
432
|
-
def
|
433
|
-
|
434
|
-
end
|
134
|
+
def check_version_supported
|
135
|
+
return if defined?(@version_checked)
|
435
136
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
137
|
+
min_version = adapter.min_version
|
138
|
+
if min_version
|
139
|
+
version = adapter.server_version
|
140
|
+
if version < Gem::Version.new(min_version)
|
141
|
+
raise UnsupportedVersion, "#{adapter.name} version (#{version}) not supported in this version of Strong Migrations (#{StrongMigrations::VERSION})"
|
440
142
|
end
|
441
143
|
end
|
442
|
-
end
|
443
|
-
|
444
|
-
def target_version(target_version)
|
445
|
-
target_version ||= StrongMigrations.target_version
|
446
|
-
version =
|
447
|
-
if target_version && StrongMigrations.developer_env?
|
448
|
-
target_version.to_s
|
449
|
-
else
|
450
|
-
yield
|
451
|
-
end
|
452
|
-
Gem::Version.new(version)
|
453
|
-
end
|
454
144
|
|
455
|
-
|
456
|
-
ActiveRecord::VERSION::STRING.to_f
|
145
|
+
@version_checked = true
|
457
146
|
end
|
458
147
|
|
459
|
-
def
|
460
|
-
|
461
|
-
|
462
|
-
if limit && !@lock_timeout_checked
|
463
|
-
if postgresql?
|
464
|
-
lock_timeout = connection.select_all("SHOW lock_timeout").first["lock_timeout"]
|
465
|
-
lock_timeout_sec = timeout_to_sec(lock_timeout)
|
466
|
-
if lock_timeout_sec == 0
|
467
|
-
warn "[strong_migrations] DANGER: No lock timeout set"
|
468
|
-
elsif lock_timeout_sec > limit
|
469
|
-
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
470
|
-
end
|
471
|
-
elsif mysql? || mariadb?
|
472
|
-
lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
|
473
|
-
# lock timeout is an integer
|
474
|
-
if lock_timeout.to_i > limit
|
475
|
-
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
476
|
-
end
|
477
|
-
end
|
478
|
-
@lock_timeout_checked = true
|
479
|
-
end
|
480
|
-
end
|
148
|
+
def set_timeouts
|
149
|
+
return if @timeouts_set
|
481
150
|
|
482
|
-
|
483
|
-
|
484
|
-
units = {
|
485
|
-
"us" => 0.001,
|
486
|
-
"ms" => 1,
|
487
|
-
"s" => 1000,
|
488
|
-
"min" => 1000 * 60,
|
489
|
-
"h" => 1000 * 60 * 60,
|
490
|
-
"d" => 1000 * 60 * 60 * 24
|
491
|
-
}
|
492
|
-
timeout_ms = timeout.to_i
|
493
|
-
units.each do |k, v|
|
494
|
-
if timeout.end_with?(k)
|
495
|
-
timeout_ms *= v
|
496
|
-
break
|
497
|
-
end
|
151
|
+
if StrongMigrations.statement_timeout
|
152
|
+
adapter.set_statement_timeout(StrongMigrations.statement_timeout)
|
498
153
|
end
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
def postgresql_timeout(timeout)
|
503
|
-
if timeout.is_a?(String)
|
504
|
-
timeout
|
505
|
-
else
|
506
|
-
# use ceil to prevent no timeout for values under 1 ms
|
507
|
-
(timeout.to_f * 1000).ceil
|
154
|
+
if StrongMigrations.lock_timeout
|
155
|
+
adapter.set_lock_timeout(StrongMigrations.lock_timeout)
|
508
156
|
end
|
509
|
-
end
|
510
157
|
|
511
|
-
|
512
|
-
query = <<~SQL
|
513
|
-
SELECT
|
514
|
-
conname AS name,
|
515
|
-
pg_get_constraintdef(oid) AS def
|
516
|
-
FROM
|
517
|
-
pg_constraint
|
518
|
-
WHERE
|
519
|
-
contype = 'c' AND
|
520
|
-
convalidated AND
|
521
|
-
conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
|
522
|
-
SQL
|
523
|
-
connection.select_all(query.squish).to_a
|
158
|
+
@timeouts_set = true
|
524
159
|
end
|
525
160
|
|
526
|
-
def
|
527
|
-
return
|
528
|
-
|
529
|
-
message = StrongMigrations.error_messages[message_key] || "Missing message"
|
530
|
-
message = message + append if append
|
161
|
+
def check_lock_timeout
|
162
|
+
return if defined?(@lock_timeout_checked)
|
531
163
|
|
532
|
-
|
533
|
-
|
534
|
-
|
164
|
+
if StrongMigrations.lock_timeout_limit
|
165
|
+
adapter.check_lock_timeout(StrongMigrations.lock_timeout_limit)
|
166
|
+
end
|
535
167
|
|
536
|
-
|
537
|
-
message = message.gsub(/%(?!{)/, "%%") % vars if message.include?("%")
|
538
|
-
@migration.stop!(message, header: header || "Dangerous operation detected")
|
168
|
+
@lock_timeout_checked = true
|
539
169
|
end
|
540
170
|
|
541
|
-
def
|
542
|
-
|
543
|
-
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
171
|
+
def safe?
|
172
|
+
self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
|
544
173
|
end
|
545
174
|
|
546
|
-
def
|
547
|
-
|
175
|
+
def version
|
176
|
+
@migration.version
|
548
177
|
end
|
549
178
|
|
550
|
-
def
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
# pretty index: {algorithm: :concurrently}
|
560
|
-
"#{k}: {#{v.map { |k2, v2| "#{k2}: #{v2.inspect}" }.join(", ")}}"
|
179
|
+
def adapter
|
180
|
+
@adapter ||= begin
|
181
|
+
cls =
|
182
|
+
case connection.adapter_name
|
183
|
+
when /postg/i # PostgreSQL, PostGIS
|
184
|
+
Adapters::PostgreSQLAdapter
|
185
|
+
when /mysql|trilogy/i
|
186
|
+
if connection.try(:mariadb?)
|
187
|
+
Adapters::MariaDBAdapter
|
561
188
|
else
|
562
|
-
|
189
|
+
Adapters::MySQLAdapter
|
563
190
|
end
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
str_args << last_arg.inspect
|
568
|
-
end
|
569
|
-
|
570
|
-
"#{command} #{str_args.join(", ")}"
|
571
|
-
end
|
572
|
-
|
573
|
-
def writes_blocked?
|
574
|
-
query = <<~SQL
|
575
|
-
SELECT
|
576
|
-
relation::regclass::text
|
577
|
-
FROM
|
578
|
-
pg_locks
|
579
|
-
WHERE
|
580
|
-
mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
|
581
|
-
pid = pg_backend_pid()
|
582
|
-
SQL
|
583
|
-
connection.select_all(query.squish).any?
|
584
|
-
end
|
191
|
+
else
|
192
|
+
Adapters::AbstractAdapter
|
193
|
+
end
|
585
194
|
|
586
|
-
|
587
|
-
|
195
|
+
cls.new(self)
|
196
|
+
end
|
588
197
|
end
|
589
198
|
|
590
|
-
def
|
591
|
-
|
592
|
-
"#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
|
199
|
+
def connection
|
200
|
+
@migration.connection
|
593
201
|
end
|
594
202
|
|
595
|
-
def
|
596
|
-
|
203
|
+
def retry_lock_timeouts?(method)
|
204
|
+
(
|
205
|
+
StrongMigrations.lock_timeout_retries > 0 &&
|
206
|
+
!in_transaction? &&
|
207
|
+
method != :transaction
|
208
|
+
)
|
597
209
|
end
|
598
210
|
end
|
599
211
|
end
|