strong_migrations 0.7.7 → 2.2.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 +153 -1
- data/LICENSE.txt +1 -1
- data/README.md +328 -201
- data/lib/generators/strong_migrations/install_generator.rb +3 -7
- data/lib/strong_migrations/adapters/abstract_adapter.rb +76 -0
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +32 -0
- data/lib/strong_migrations/adapters/mysql_adapter.rb +112 -0
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +232 -0
- data/lib/strong_migrations/checker.rb +186 -511
- data/lib/strong_migrations/checks.rb +475 -0
- data/lib/strong_migrations/error_messages.rb +260 -0
- data/lib/strong_migrations/migration.rb +17 -3
- data/lib/strong_migrations/{database_tasks.rb → migration_context.rb} +20 -2
- data/lib/strong_migrations/migrator.rb +21 -0
- data/lib/strong_migrations/safe_methods.rb +48 -50
- data/lib/strong_migrations/schema_dumper.rb +32 -0
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +44 -228
- data/lib/tasks/strong_migrations.rake +2 -7
- metadata +16 -83
- data/lib/strong_migrations/alphabetize_columns.rb +0 -11
@@ -1,602 +1,277 @@
|
|
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
|
14
|
+
reset
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset
|
9
18
|
@new_tables = []
|
10
|
-
@
|
19
|
+
@new_columns = []
|
11
20
|
@timeouts_set = false
|
12
|
-
@
|
21
|
+
@committed = false
|
22
|
+
@transaction_disabled = false
|
23
|
+
@skip_retries = false
|
13
24
|
end
|
14
25
|
|
15
|
-
def safety_assured
|
16
|
-
previous_value =
|
26
|
+
def self.safety_assured
|
27
|
+
previous_value = safe
|
17
28
|
begin
|
18
|
-
|
29
|
+
self.safe = true
|
19
30
|
yield
|
20
31
|
ensure
|
21
|
-
|
32
|
+
self.safe = previous_value
|
22
33
|
end
|
23
34
|
end
|
24
35
|
|
25
|
-
def perform(method, *args)
|
36
|
+
def perform(method, *args, &block)
|
37
|
+
return yield if skip?
|
38
|
+
|
39
|
+
check_adapter
|
40
|
+
check_version_supported
|
26
41
|
set_timeouts
|
27
42
|
check_lock_timeout
|
28
43
|
|
29
44
|
if !safe? || safe_by_default_method?(method)
|
45
|
+
# TODO better pattern
|
46
|
+
# see checks.rb for methods
|
30
47
|
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
|
48
|
+
when :add_check_constraint
|
49
|
+
check_add_check_constraint(*args)
|
84
50
|
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
|
51
|
+
check_add_column(*args)
|
52
|
+
when :add_exclusion_constraint
|
53
|
+
check_add_exclusion_constraint(*args)
|
54
|
+
when :add_foreign_key
|
55
|
+
check_add_foreign_key(*args)
|
56
|
+
when :add_index
|
57
|
+
check_add_index(*args)
|
58
|
+
when :add_reference, :add_belongs_to
|
59
|
+
check_add_reference(method, *args)
|
60
|
+
when :add_unique_constraint
|
61
|
+
check_add_unique_constraint(*args)
|
111
62
|
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
|
63
|
+
check_change_column(*args)
|
64
|
+
when :change_column_default
|
65
|
+
check_change_column_default(*args)
|
66
|
+
when :change_column_null
|
67
|
+
check_change_column_null(*args)
|
68
|
+
when :change_table
|
69
|
+
check_change_table
|
70
|
+
when :create_join_table
|
71
|
+
check_create_join_table(*args)
|
173
72
|
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
|
73
|
+
check_create_table(*args)
|
214
74
|
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
|
75
|
+
check_execute
|
76
|
+
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
|
77
|
+
check_remove_column(method, *args)
|
78
|
+
when :remove_index
|
79
|
+
check_remove_index(*args)
|
80
|
+
when :rename_column
|
81
|
+
check_rename_column
|
82
|
+
when :rename_table
|
83
|
+
check_rename_table
|
328
84
|
when :validate_check_constraint
|
329
|
-
|
330
|
-
|
331
|
-
|
85
|
+
check_validate_check_constraint
|
86
|
+
when :validate_foreign_key
|
87
|
+
check_validate_foreign_key
|
88
|
+
when :commit_db_transaction
|
89
|
+
# if committed, likely no longer in DDL transaction
|
90
|
+
# and no longer eligible to be retried at migration level
|
91
|
+
# okay to have false positives
|
92
|
+
@committed = true
|
332
93
|
end
|
333
94
|
|
334
|
-
|
335
|
-
|
95
|
+
if !safe?
|
96
|
+
# custom checks
|
97
|
+
StrongMigrations.checks.each do |check|
|
98
|
+
@migration.instance_exec(method, args, &check)
|
99
|
+
end
|
336
100
|
end
|
337
101
|
end
|
338
102
|
|
339
|
-
result =
|
103
|
+
result =
|
104
|
+
if retry_lock_timeouts?(method)
|
105
|
+
# TODO figure out how to handle methods that generate multiple statements
|
106
|
+
# like add_reference(table, ref, index: {algorithm: :concurrently})
|
107
|
+
# lock timeout after first statement will cause retry to fail
|
108
|
+
retry_lock_timeouts { perform_method(method, *args, &block) }
|
109
|
+
else
|
110
|
+
perform_method(method, *args, &block)
|
111
|
+
end
|
340
112
|
|
341
113
|
# outdated statistics + a new index can hurt performance of existing queries
|
342
114
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
343
|
-
analyze_table(args[0])
|
115
|
+
adapter.analyze_table(args[0])
|
344
116
|
end
|
345
117
|
|
346
118
|
result
|
347
119
|
end
|
348
120
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
if !@timeouts_set
|
353
|
-
if StrongMigrations.statement_timeout
|
354
|
-
statement =
|
355
|
-
if postgresql?
|
356
|
-
"SET statement_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.statement_timeout))}"
|
357
|
-
elsif mysql?
|
358
|
-
# use ceil to prevent no timeout for values under 1 ms
|
359
|
-
"SET max_execution_time = #{connection.quote((StrongMigrations.statement_timeout.to_f * 1000).ceil)}"
|
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)
|
380
|
-
end
|
381
|
-
|
382
|
-
@timeouts_set = true
|
121
|
+
def perform_method(method, *args)
|
122
|
+
if StrongMigrations.remove_invalid_indexes && direction == :up && method == :add_index && postgresql?
|
123
|
+
remove_invalid_index_if_needed(*args)
|
383
124
|
end
|
125
|
+
yield
|
384
126
|
end
|
385
127
|
|
386
|
-
def
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
128
|
+
def retry_lock_timeouts(check_committed: false)
|
129
|
+
retries = 0
|
130
|
+
begin
|
131
|
+
yield
|
132
|
+
rescue ActiveRecord::LockWaitTimeout => e
|
133
|
+
if retries < StrongMigrations.lock_timeout_retries && !(check_committed && @committed)
|
134
|
+
retries += 1
|
135
|
+
delay = StrongMigrations.lock_timeout_retry_delay
|
136
|
+
@migration.say("Lock timeout. Retrying in #{delay} seconds...")
|
137
|
+
sleep(delay)
|
138
|
+
retry
|
139
|
+
end
|
140
|
+
raise e
|
141
|
+
end
|
396
142
|
end
|
397
143
|
|
398
144
|
def version_safe?
|
399
145
|
version && version <= StrongMigrations.start_after
|
400
146
|
end
|
401
147
|
|
402
|
-
def
|
403
|
-
|
148
|
+
def skip?
|
149
|
+
StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
|
404
150
|
end
|
405
151
|
|
406
|
-
|
407
|
-
@postgresql_version ||= begin
|
408
|
-
target_version(StrongMigrations.target_postgresql_version) do
|
409
|
-
# only works with major versions
|
410
|
-
connection.select_all("SHOW server_version_num").first["server_version_num"].to_i / 10000
|
411
|
-
end
|
412
|
-
end
|
413
|
-
end
|
152
|
+
private
|
414
153
|
|
415
|
-
def
|
416
|
-
|
417
|
-
end
|
154
|
+
def check_adapter
|
155
|
+
return if defined?(@adapter_checked)
|
418
156
|
|
419
|
-
|
420
|
-
|
421
|
-
target_version(StrongMigrations.target_mysql_version) do
|
422
|
-
connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
423
|
-
end
|
157
|
+
if adapter.instance_of?(Adapters::AbstractAdapter)
|
158
|
+
warn "[strong_migrations] Unsupported adapter: #{connection.adapter_name}. Use StrongMigrations.skip_database(#{db_config_name.to_sym.inspect}) to silence this warning."
|
424
159
|
end
|
425
|
-
end
|
426
160
|
|
427
|
-
|
428
|
-
connection.adapter_name =~ /mysql/i && connection.try(:mariadb?)
|
161
|
+
@adapter_checked = true
|
429
162
|
end
|
430
163
|
|
431
|
-
def
|
432
|
-
|
433
|
-
target_version(StrongMigrations.target_mariadb_version) do
|
434
|
-
connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
435
|
-
end
|
436
|
-
end
|
437
|
-
end
|
164
|
+
def check_version_supported
|
165
|
+
return if defined?(@version_checked)
|
438
166
|
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
if
|
443
|
-
|
444
|
-
else
|
445
|
-
yield
|
167
|
+
min_version = adapter.min_version
|
168
|
+
if min_version
|
169
|
+
version = adapter.server_version
|
170
|
+
if version < Gem::Version.new(min_version)
|
171
|
+
raise UnsupportedVersion, "#{adapter.name} version (#{version}) not supported in this version of Strong Migrations (#{StrongMigrations::VERSION})"
|
446
172
|
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
|
474
173
|
end
|
475
|
-
end
|
476
174
|
|
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
|
175
|
+
@version_checked = true
|
495
176
|
end
|
496
177
|
|
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
|
178
|
+
def set_timeouts
|
179
|
+
return if @timeouts_set
|
505
180
|
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
181
|
+
if StrongMigrations.statement_timeout
|
182
|
+
adapter.set_statement_timeout(StrongMigrations.statement_timeout)
|
183
|
+
end
|
184
|
+
if StrongMigrations.lock_timeout
|
185
|
+
adapter.set_lock_timeout(StrongMigrations.lock_timeout)
|
511
186
|
end
|
512
|
-
end
|
513
187
|
|
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
|
188
|
+
@timeouts_set = true
|
527
189
|
end
|
528
190
|
|
529
|
-
def
|
530
|
-
return
|
531
|
-
|
532
|
-
message = StrongMigrations.error_messages[message_key] || "Missing message"
|
533
|
-
message = message + append if append
|
191
|
+
def check_lock_timeout
|
192
|
+
return if defined?(@lock_timeout_checked)
|
534
193
|
|
535
|
-
|
536
|
-
|
537
|
-
|
194
|
+
if StrongMigrations.lock_timeout_limit
|
195
|
+
adapter.check_lock_timeout(StrongMigrations.lock_timeout_limit)
|
196
|
+
end
|
538
197
|
|
539
|
-
|
540
|
-
message = message.gsub(/%(?!{)/, "%%") % vars if message.include?("%")
|
541
|
-
@migration.stop!(message, header: header || "Dangerous operation detected")
|
198
|
+
@lock_timeout_checked = true
|
542
199
|
end
|
543
200
|
|
544
|
-
def
|
545
|
-
|
546
|
-
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
201
|
+
def safe?
|
202
|
+
self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
|
547
203
|
end
|
548
204
|
|
549
|
-
def
|
550
|
-
|
205
|
+
def version
|
206
|
+
@migration.version
|
551
207
|
end
|
552
208
|
|
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(", ")}}"
|
209
|
+
def adapter
|
210
|
+
@adapter ||= begin
|
211
|
+
cls =
|
212
|
+
case connection.adapter_name
|
213
|
+
when /postg/i # PostgreSQL, PostGIS
|
214
|
+
Adapters::PostgreSQLAdapter
|
215
|
+
when /mysql|trilogy/i
|
216
|
+
if connection.try(:mariadb?)
|
217
|
+
Adapters::MariaDBAdapter
|
564
218
|
else
|
565
|
-
|
219
|
+
Adapters::MySQLAdapter
|
566
220
|
end
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
221
|
+
else
|
222
|
+
Adapters::AbstractAdapter
|
223
|
+
end
|
224
|
+
|
225
|
+
cls.new(self)
|
571
226
|
end
|
227
|
+
end
|
572
228
|
|
573
|
-
|
229
|
+
def connection
|
230
|
+
@migration.connection
|
574
231
|
end
|
575
232
|
|
576
|
-
def
|
577
|
-
|
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?
|
233
|
+
def db_config_name
|
234
|
+
connection.pool.db_config.name
|
587
235
|
end
|
588
236
|
|
589
|
-
def
|
590
|
-
|
237
|
+
def retry_lock_timeouts?(method)
|
238
|
+
(
|
239
|
+
StrongMigrations.lock_timeout_retries > 0 &&
|
240
|
+
!in_transaction? &&
|
241
|
+
method != :transaction &&
|
242
|
+
!@skip_retries
|
243
|
+
)
|
591
244
|
end
|
592
245
|
|
593
|
-
def
|
594
|
-
|
595
|
-
|
246
|
+
def without_retries
|
247
|
+
previous_value = @skip_retries
|
248
|
+
begin
|
249
|
+
@skip_retries = true
|
250
|
+
yield
|
251
|
+
ensure
|
252
|
+
@skip_retries = previous_value
|
253
|
+
end
|
596
254
|
end
|
597
255
|
|
598
|
-
|
599
|
-
|
256
|
+
# REINDEX INDEX CONCURRENTLY leaves a new invalid index if it fails, so use remove_index instead
|
257
|
+
def remove_invalid_index_if_needed(*args)
|
258
|
+
options = args.extract_options!
|
259
|
+
|
260
|
+
# ensures has same options as existing index
|
261
|
+
# check args to avoid errors with index_exists?
|
262
|
+
return unless args.size == 2 && connection.index_exists?(*args, **options.merge(valid: false))
|
263
|
+
|
264
|
+
table, columns = args
|
265
|
+
index_name = options.fetch(:name, connection.index_name(table, columns))
|
266
|
+
|
267
|
+
# valid option is ignored for Active Record < 7.1, so check name as well
|
268
|
+
return if ar_version < 7.1 && !adapter.index_invalid?(table, index_name)
|
269
|
+
|
270
|
+
@migration.say("Attempting to remove invalid index")
|
271
|
+
without_retries do
|
272
|
+
# TODO pass index schema for extra safety?
|
273
|
+
@migration.remove_index(table, **{name: index_name}.merge(options.slice(:algorithm)))
|
274
|
+
end
|
600
275
|
end
|
601
276
|
end
|
602
277
|
end
|