strong_migrations 0.7.0 → 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 +183 -1
- data/LICENSE.txt +1 -1
- data/README.md +385 -164
- data/lib/generators/strong_migrations/install_generator.rb +25 -0
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +10 -3
- 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 +190 -440
- data/lib/strong_migrations/checks.rb +475 -0
- data/lib/strong_migrations/error_messages.rb +260 -0
- data/lib/strong_migrations/migration.rb +20 -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 +123 -0
- data/lib/strong_migrations/schema_dumper.rb +32 -0
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +46 -203
- data/lib/tasks/strong_migrations.rake +2 -7
- metadata +17 -83
- data/lib/strong_migrations/alphabetize_columns.rb +0 -11
@@ -1,527 +1,277 @@
|
|
1
1
|
module StrongMigrations
|
2
2
|
class Checker
|
3
|
-
|
3
|
+
include Checks
|
4
|
+
include SafeMethods
|
5
|
+
|
6
|
+
attr_accessor :direction, :transaction_disabled, :timeouts_set
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :safe
|
10
|
+
end
|
4
11
|
|
5
12
|
def initialize(migration)
|
6
13
|
@migration = migration
|
14
|
+
reset
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset
|
7
18
|
@new_tables = []
|
8
|
-
@
|
19
|
+
@new_columns = []
|
9
20
|
@timeouts_set = false
|
10
|
-
@
|
21
|
+
@committed = false
|
22
|
+
@transaction_disabled = false
|
23
|
+
@skip_retries = false
|
11
24
|
end
|
12
25
|
|
13
|
-
def safety_assured
|
14
|
-
previous_value =
|
26
|
+
def self.safety_assured
|
27
|
+
previous_value = safe
|
15
28
|
begin
|
16
|
-
|
29
|
+
self.safe = true
|
17
30
|
yield
|
18
31
|
ensure
|
19
|
-
|
32
|
+
self.safe = previous_value
|
20
33
|
end
|
21
34
|
end
|
22
35
|
|
23
|
-
def perform(method, *args)
|
36
|
+
def perform(method, *args, &block)
|
37
|
+
return yield if skip?
|
38
|
+
|
39
|
+
check_adapter
|
40
|
+
check_version_supported
|
24
41
|
set_timeouts
|
25
42
|
check_lock_timeout
|
26
43
|
|
27
|
-
|
44
|
+
if !safe? || safe_by_default_method?(method)
|
45
|
+
# TODO better pattern
|
46
|
+
# see checks.rb for methods
|
28
47
|
case method
|
29
|
-
when :
|
30
|
-
|
31
|
-
case method
|
32
|
-
when :remove_timestamps
|
33
|
-
["created_at", "updated_at"]
|
34
|
-
when :remove_column
|
35
|
-
[args[1].to_s]
|
36
|
-
when :remove_columns
|
37
|
-
args[1..-1].map(&:to_s)
|
38
|
-
else
|
39
|
-
options = args[2] || {}
|
40
|
-
reference = args[1]
|
41
|
-
cols = []
|
42
|
-
cols << "#{reference}_type" if options[:polymorphic]
|
43
|
-
cols << "#{reference}_id"
|
44
|
-
cols
|
45
|
-
end
|
46
|
-
|
47
|
-
code = "self.ignored_columns = #{columns.inspect}"
|
48
|
-
|
49
|
-
raise_error :remove_column,
|
50
|
-
model: args[0].to_s.classify,
|
51
|
-
code: code,
|
52
|
-
command: command_str(method, args),
|
53
|
-
column_suffix: columns.size > 1 ? "s" : ""
|
54
|
-
when :change_table
|
55
|
-
raise_error :change_table, header: "Possibly dangerous operation"
|
56
|
-
when :rename_table
|
57
|
-
raise_error :rename_table
|
58
|
-
when :rename_column
|
59
|
-
raise_error :rename_column
|
60
|
-
when :add_index
|
61
|
-
table, columns, options = args
|
62
|
-
options ||= {}
|
63
|
-
|
64
|
-
if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
|
65
|
-
raise_error :add_index_columns, header: "Best practice"
|
66
|
-
end
|
67
|
-
if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
|
68
|
-
raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
|
69
|
-
end
|
70
|
-
when :remove_index
|
71
|
-
table, options = args
|
72
|
-
unless options.is_a?(Hash)
|
73
|
-
options = {column: options}
|
74
|
-
end
|
75
|
-
options ||= {}
|
76
|
-
|
77
|
-
if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
|
78
|
-
raise_error :remove_index, command: command_str("remove_index", [table, options.merge(algorithm: :concurrently)])
|
79
|
-
end
|
48
|
+
when :add_check_constraint
|
49
|
+
check_add_check_constraint(*args)
|
80
50
|
when :add_column
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
93
|
-
|
94
|
-
raise_error :add_column_default,
|
95
|
-
add_command: command_str("add_column", [table, column, type, options.except(:default)]),
|
96
|
-
change_command: command_str("change_column_default", [table, column, default]),
|
97
|
-
remove_command: command_str("remove_column", [table, column]),
|
98
|
-
code: backfill_code(table, column, default),
|
99
|
-
append: append,
|
100
|
-
rewrite_blocks: rewrite_blocks
|
101
|
-
end
|
102
|
-
|
103
|
-
if type.to_s == "json" && postgresql?
|
104
|
-
raise_error :add_column_json,
|
105
|
-
command: command_str("add_column", [table, column, :jsonb, options])
|
106
|
-
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)
|
107
62
|
when :change_column
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
when "string"
|
118
|
-
# safe to increase limit or remove it
|
119
|
-
# not safe to decrease limit or add a limit
|
120
|
-
case existing_type
|
121
|
-
when "character varying"
|
122
|
-
safe = !options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
|
123
|
-
when "text"
|
124
|
-
safe = !options[:limit]
|
125
|
-
end
|
126
|
-
when "text"
|
127
|
-
# safe to change varchar to text (and text to text)
|
128
|
-
safe = ["character varying", "text"].include?(existing_type)
|
129
|
-
when "numeric", "decimal"
|
130
|
-
# numeric and decimal are equivalent and can be used interchangably
|
131
|
-
safe = ["numeric", "decimal"].include?(existing_type) &&
|
132
|
-
(
|
133
|
-
(
|
134
|
-
# unconstrained
|
135
|
-
!options[:precision] && !options[:scale]
|
136
|
-
) || (
|
137
|
-
# increased precision, same scale
|
138
|
-
options[:precision] && existing_column.precision &&
|
139
|
-
options[:precision] >= existing_column.precision &&
|
140
|
-
options[:scale] == existing_column.scale
|
141
|
-
)
|
142
|
-
)
|
143
|
-
when "datetime", "timestamp", "timestamptz"
|
144
|
-
safe = ["timestamp without time zone", "timestamp with time zone"].include?(existing_type) &&
|
145
|
-
postgresql_version >= Gem::Version.new("12") &&
|
146
|
-
connection.select_all("SHOW timezone").first["TimeZone"] == "UTC"
|
147
|
-
end
|
148
|
-
elsif mysql? || mariadb?
|
149
|
-
case type.to_s
|
150
|
-
when "string"
|
151
|
-
# https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html
|
152
|
-
# https://mariadb.com/kb/en/innodb-online-ddl-operations-with-the-instant-alter-algorithm/#changing-the-data-type-of-a-column
|
153
|
-
# increased limit, but doesn't change number of length bytes
|
154
|
-
# 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
|
155
|
-
limit = options[:limit] || 255
|
156
|
-
safe = ["varchar"].include?(existing_type) &&
|
157
|
-
limit >= existing_column.limit &&
|
158
|
-
(limit <= 255 || existing_column.limit > 255)
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# unsafe to set NOT NULL for safe types
|
164
|
-
if safe && existing_column.null && options[:null] == false
|
165
|
-
raise_error :change_column_with_not_null
|
166
|
-
end
|
167
|
-
|
168
|
-
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)
|
169
72
|
when :create_table
|
170
|
-
|
171
|
-
options ||= {}
|
172
|
-
|
173
|
-
raise_error :create_table if options[:force]
|
174
|
-
|
175
|
-
# keep track of new tables of add_index check
|
176
|
-
@new_tables << table.to_s
|
177
|
-
when :add_reference, :add_belongs_to
|
178
|
-
table, reference, options = args
|
179
|
-
options ||= {}
|
180
|
-
|
181
|
-
if postgresql?
|
182
|
-
index_value = options.fetch(:index, true)
|
183
|
-
concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently
|
184
|
-
bad_index = index_value && !concurrently_set
|
185
|
-
|
186
|
-
if bad_index || options[:foreign_key]
|
187
|
-
columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
|
188
|
-
|
189
|
-
if index_value.is_a?(Hash)
|
190
|
-
options[:index] = options[:index].merge(algorithm: :concurrently)
|
191
|
-
else
|
192
|
-
options = options.merge(index: {algorithm: :concurrently})
|
193
|
-
end
|
194
|
-
|
195
|
-
if options.delete(:foreign_key)
|
196
|
-
headline = "Adding a foreign key blocks writes on both tables."
|
197
|
-
append = "
|
198
|
-
|
199
|
-
Then add the foreign key in separate migrations."
|
200
|
-
else
|
201
|
-
headline = "Adding an index non-concurrently locks the table."
|
202
|
-
end
|
203
|
-
|
204
|
-
raise_error :add_reference,
|
205
|
-
headline: headline,
|
206
|
-
command: command_str(method, [table, reference, options]),
|
207
|
-
append: append
|
208
|
-
end
|
209
|
-
end
|
73
|
+
check_create_table(*args)
|
210
74
|
when :execute
|
211
|
-
|
212
|
-
when :
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
unless safe
|
223
|
-
# match https://github.com/nullobject/rein
|
224
|
-
constraint_name = "#{table}_#{column}_null"
|
225
|
-
|
226
|
-
validate_constraint_code = String.new(constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name]))
|
227
|
-
if postgresql_version >= Gem::Version.new("12")
|
228
|
-
validate_constraint_code << "\n #{command_str(:change_column_null, [table, column, null])}"
|
229
|
-
validate_constraint_code << "\n #{constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])}"
|
230
|
-
end
|
231
|
-
|
232
|
-
raise_error :change_column_null_postgresql,
|
233
|
-
add_constraint_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
|
234
|
-
validate_constraint_code: validate_constraint_code
|
235
|
-
end
|
236
|
-
elsif mysql? || mariadb?
|
237
|
-
raise_error :change_column_null_mysql
|
238
|
-
elsif !default.nil?
|
239
|
-
raise_error :change_column_null,
|
240
|
-
code: backfill_code(table, column, default)
|
241
|
-
end
|
242
|
-
end
|
243
|
-
when :add_foreign_key
|
244
|
-
from_table, to_table, options = args
|
245
|
-
options ||= {}
|
246
|
-
|
247
|
-
# always validated before 5.2
|
248
|
-
validate = options.fetch(:validate, true) || ActiveRecord::VERSION::STRING < "5.2"
|
249
|
-
|
250
|
-
if postgresql? && validate
|
251
|
-
if ActiveRecord::VERSION::STRING < "5.2"
|
252
|
-
# fk name logic from rails
|
253
|
-
primary_key = options[:primary_key] || "id"
|
254
|
-
column = options[:column] || "#{to_table.to_s.singularize}_id"
|
255
|
-
hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10)
|
256
|
-
fk_name = options[:name] || "fk_rails_#{hashed_identifier}"
|
257
|
-
|
258
|
-
raise_error :add_foreign_key,
|
259
|
-
add_foreign_key_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]),
|
260
|
-
validate_foreign_key_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
|
261
|
-
else
|
262
|
-
raise_error :add_foreign_key,
|
263
|
-
add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
|
264
|
-
validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
|
265
|
-
end
|
266
|
-
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
|
84
|
+
when :validate_check_constraint
|
85
|
+
check_validate_check_constraint
|
267
86
|
when :validate_foreign_key
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
271
93
|
end
|
272
94
|
|
273
|
-
|
274
|
-
|
95
|
+
if !safe?
|
96
|
+
# custom checks
|
97
|
+
StrongMigrations.checks.each do |check|
|
98
|
+
@migration.instance_exec(method, args, &check)
|
99
|
+
end
|
275
100
|
end
|
276
101
|
end
|
277
102
|
|
278
|
-
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
|
279
112
|
|
280
113
|
# outdated statistics + a new index can hurt performance of existing queries
|
281
114
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
282
|
-
|
283
|
-
connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}"
|
284
|
-
elsif mariadb? || mysql?
|
285
|
-
connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
|
286
|
-
end
|
115
|
+
adapter.analyze_table(args[0])
|
287
116
|
end
|
288
117
|
|
289
118
|
result
|
290
119
|
end
|
291
120
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
if !@timeouts_set
|
296
|
-
if StrongMigrations.statement_timeout
|
297
|
-
statement =
|
298
|
-
if postgresql?
|
299
|
-
"SET statement_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.statement_timeout))}"
|
300
|
-
elsif mysql?
|
301
|
-
"SET max_execution_time = #{connection.quote(StrongMigrations.statement_timeout.to_i * 1000)}"
|
302
|
-
elsif mariadb?
|
303
|
-
"SET max_statement_time = #{connection.quote(StrongMigrations.statement_timeout)}"
|
304
|
-
else
|
305
|
-
raise StrongMigrations::Error, "Statement timeout not supported for this database"
|
306
|
-
end
|
307
|
-
|
308
|
-
connection.select_all(statement)
|
309
|
-
end
|
310
|
-
|
311
|
-
if StrongMigrations.lock_timeout
|
312
|
-
statement =
|
313
|
-
if postgresql?
|
314
|
-
"SET lock_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.lock_timeout))}"
|
315
|
-
elsif mysql? || mariadb?
|
316
|
-
"SET lock_wait_timeout = #{connection.quote(StrongMigrations.lock_timeout)}"
|
317
|
-
else
|
318
|
-
raise StrongMigrations::Error, "Lock timeout not supported for this database"
|
319
|
-
end
|
320
|
-
|
321
|
-
connection.select_all(statement)
|
322
|
-
end
|
323
|
-
|
324
|
-
@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)
|
325
124
|
end
|
125
|
+
yield
|
326
126
|
end
|
327
127
|
|
328
|
-
def
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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
|
339
142
|
end
|
340
143
|
|
341
144
|
def version_safe?
|
342
145
|
version && version <= StrongMigrations.start_after
|
343
146
|
end
|
344
147
|
|
345
|
-
def
|
346
|
-
|
148
|
+
def skip?
|
149
|
+
StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
|
347
150
|
end
|
348
151
|
|
349
|
-
|
350
|
-
@postgresql_version ||= begin
|
351
|
-
target_version(StrongMigrations.target_postgresql_version) do
|
352
|
-
# only works with major versions
|
353
|
-
connection.select_all("SHOW server_version_num").first["server_version_num"].to_i / 10000
|
354
|
-
end
|
355
|
-
end
|
356
|
-
end
|
152
|
+
private
|
357
153
|
|
358
|
-
def
|
359
|
-
|
360
|
-
end
|
154
|
+
def check_adapter
|
155
|
+
return if defined?(@adapter_checked)
|
361
156
|
|
362
|
-
|
363
|
-
|
364
|
-
target_version(StrongMigrations.target_mysql_version) do
|
365
|
-
connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
|
366
|
-
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."
|
367
159
|
end
|
368
|
-
end
|
369
160
|
|
370
|
-
|
371
|
-
connection.adapter_name =~ /mysql/i && connection.try(:mariadb?)
|
161
|
+
@adapter_checked = true
|
372
162
|
end
|
373
163
|
|
374
|
-
def
|
375
|
-
|
376
|
-
|
377
|
-
|
164
|
+
def check_version_supported
|
165
|
+
return if defined?(@version_checked)
|
166
|
+
|
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})"
|
378
172
|
end
|
379
173
|
end
|
380
|
-
end
|
381
174
|
|
382
|
-
|
383
|
-
version =
|
384
|
-
if target_version && StrongMigrations.developer_env?
|
385
|
-
target_version.to_s
|
386
|
-
else
|
387
|
-
yield
|
388
|
-
end
|
389
|
-
Gem::Version.new(version)
|
175
|
+
@version_checked = true
|
390
176
|
end
|
391
177
|
|
392
|
-
def
|
393
|
-
|
394
|
-
|
395
|
-
if limit && !@lock_timeout_checked
|
396
|
-
if postgresql?
|
397
|
-
lock_timeout = connection.select_all("SHOW lock_timeout").first["lock_timeout"]
|
398
|
-
lock_timeout_sec = timeout_to_sec(lock_timeout)
|
399
|
-
if lock_timeout_sec == 0
|
400
|
-
warn "[strong_migrations] DANGER: No lock timeout set"
|
401
|
-
elsif lock_timeout_sec > limit
|
402
|
-
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
403
|
-
end
|
404
|
-
elsif mysql? || mariadb?
|
405
|
-
lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
|
406
|
-
if lock_timeout.to_i > limit
|
407
|
-
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
408
|
-
end
|
409
|
-
end
|
410
|
-
@lock_timeout_checked = true
|
411
|
-
end
|
412
|
-
end
|
178
|
+
def set_timeouts
|
179
|
+
return if @timeouts_set
|
413
180
|
|
414
|
-
|
415
|
-
|
416
|
-
units = {
|
417
|
-
"us" => 0.001,
|
418
|
-
"ms" => 1,
|
419
|
-
"s" => 1000,
|
420
|
-
"min" => 1000 * 60,
|
421
|
-
"h" => 1000 * 60 * 60,
|
422
|
-
"d" => 1000 * 60 * 60 * 24
|
423
|
-
}
|
424
|
-
timeout_ms = timeout.to_i
|
425
|
-
units.each do |k, v|
|
426
|
-
if timeout.end_with?(k)
|
427
|
-
timeout_ms *= v
|
428
|
-
break
|
429
|
-
end
|
181
|
+
if StrongMigrations.statement_timeout
|
182
|
+
adapter.set_statement_timeout(StrongMigrations.statement_timeout)
|
430
183
|
end
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
def postgresql_timeout(timeout)
|
435
|
-
if timeout.is_a?(String)
|
436
|
-
timeout
|
437
|
-
else
|
438
|
-
timeout.to_i * 1000
|
184
|
+
if StrongMigrations.lock_timeout
|
185
|
+
adapter.set_lock_timeout(StrongMigrations.lock_timeout)
|
439
186
|
end
|
440
|
-
end
|
441
187
|
|
442
|
-
|
443
|
-
query = <<~SQL
|
444
|
-
SELECT
|
445
|
-
conname AS name,
|
446
|
-
pg_get_constraintdef(oid) AS def
|
447
|
-
FROM
|
448
|
-
pg_constraint
|
449
|
-
WHERE
|
450
|
-
contype = 'c' AND
|
451
|
-
convalidated AND
|
452
|
-
conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
|
453
|
-
SQL
|
454
|
-
connection.select_all(query.squish).to_a
|
188
|
+
@timeouts_set = true
|
455
189
|
end
|
456
190
|
|
457
|
-
def
|
458
|
-
return
|
191
|
+
def check_lock_timeout
|
192
|
+
return if defined?(@lock_timeout_checked)
|
459
193
|
|
460
|
-
|
461
|
-
|
194
|
+
if StrongMigrations.lock_timeout_limit
|
195
|
+
adapter.check_lock_timeout(StrongMigrations.lock_timeout_limit)
|
196
|
+
end
|
462
197
|
|
463
|
-
|
464
|
-
|
465
|
-
vars[:base_model] = "ApplicationRecord"
|
198
|
+
@lock_timeout_checked = true
|
199
|
+
end
|
466
200
|
|
467
|
-
|
468
|
-
|
469
|
-
@migration.stop!(message, header: header || "Dangerous operation detected")
|
201
|
+
def safe?
|
202
|
+
self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
|
470
203
|
end
|
471
204
|
|
472
|
-
def
|
473
|
-
|
474
|
-
code = statement % identifiers.map { |v| connection.quote_table_name(v) }
|
475
|
-
"safety_assured do\n execute '#{code}' \n end"
|
205
|
+
def version
|
206
|
+
@migration.version
|
476
207
|
end
|
477
208
|
|
478
|
-
def
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
# pretty index: {algorithm: :concurrently}
|
488
|
-
"#{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
|
489
218
|
else
|
490
|
-
|
219
|
+
Adapters::MySQLAdapter
|
491
220
|
end
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
221
|
+
else
|
222
|
+
Adapters::AbstractAdapter
|
223
|
+
end
|
224
|
+
|
225
|
+
cls.new(self)
|
496
226
|
end
|
227
|
+
end
|
497
228
|
|
498
|
-
|
229
|
+
def connection
|
230
|
+
@migration.connection
|
499
231
|
end
|
500
232
|
|
501
|
-
def
|
502
|
-
|
503
|
-
SELECT
|
504
|
-
relation::regclass::text
|
505
|
-
FROM
|
506
|
-
pg_locks
|
507
|
-
WHERE
|
508
|
-
mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
|
509
|
-
pid = pg_backend_pid()
|
510
|
-
SQL
|
511
|
-
connection.select_all(query.squish).any?
|
233
|
+
def db_config_name
|
234
|
+
connection.pool.db_config.name
|
512
235
|
end
|
513
236
|
|
514
|
-
def
|
515
|
-
|
237
|
+
def retry_lock_timeouts?(method)
|
238
|
+
(
|
239
|
+
StrongMigrations.lock_timeout_retries > 0 &&
|
240
|
+
!in_transaction? &&
|
241
|
+
method != :transaction &&
|
242
|
+
!@skip_retries
|
243
|
+
)
|
516
244
|
end
|
517
245
|
|
518
|
-
def
|
519
|
-
|
520
|
-
|
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
|
521
254
|
end
|
522
255
|
|
523
|
-
|
524
|
-
|
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
|
525
275
|
end
|
526
276
|
end
|
527
277
|
end
|